目录
什么是事务?
事务:是一组操作的集合,这组操作要么全部成功,要么全部失败,以确保数据的一致性和完整性
例如:
在进行转账操作时(A向B转账):
第一步:A 账户 -2000 元
第二步:B账户 +2000 元
对应 SQL:
update account set balance = balance - 2000 where name = 'A';
update account set balance = balance + 2000 where name = 'B';
若没有事务,第一个SQL执行成功,但在执行第二个SQL语句之前,数据库挂了,此时, A 账户中的 2000 元扣除掉,但是还未向 B 账号中转入
等待数据库恢复时,A的钱就被扣掉了,但是B并没有收到钱
此时就需要使用事务,让 A-2000 B+2000 作为一组操作,这组操作要么一起成功,要么一起失败(让所有数据恢复原样)
事务的特性
事务有四个核心的特性:
(1)原子性:通过事务,将多个操作打包为一个整体
(2)一致性:在事务执行前后,数据库的状态必须保持一致,防止出现上述数据库中间出现问题,产生 钱消失了 这样的不合理的情况
(3)持久性:事务的任何修改,都是持久化存在的(写入硬盘的),无论是重启程序,还是重启主机,修改的数据都是不会丢失的
(4)隔离性:多个事务在并发执行时,可能会存在一定的问题,因此,我们就需要具体情况具体分析,判断需要当前数据尽量准确,还是希望执行速度尽可能的快
接下来,我们就来学习事务的隔离级别
事务的隔离级别
脏读
我们来看一个例子:
有两个事务A和B
事务A修改了某个数据,但是事务还未提交(告诉数据库服务器自己已经执行完毕)
此时,事务B读取了事务A修改的数据
但在事务B读取完之后,事务A再对这个数据进行了修改,然后才提交数据
即:
在这个过程中,事务B读取到的数据并不是最终的数据,也就是一个 脏的数据
要解决脏读问题,核心思路是降低事务的并发程度,也就是给写操作加锁,也就是写的时候不能进行读,只有等写完,并且提交事务(释放锁)之后,其他事务才能进行读取
此时就意味着,在事务A释放锁之前,事务B不能进行访问,只有等事务A释放锁之后,事务B才能读取
不可重复读
不可重复读,是在上述加写锁的前提下,可能会出现的问题
虽然我们对写加上了锁,但可以通过多个事务,多次提交的方式来修改数据
事务A先修改了数据(对写加锁,此时事务B必须等待事务A提交之后才能读取),事务A提交,事务B开始读取数据(事务B多次读取数据)
此时事务C对上述数据再次进行修改,这就导致事务B两次读取到的结果并不相同
出现上述问题是因为我们只对写加了锁,约定写的时候不能读,但是没有对读加锁,即未表明读的时候不能写,此时,也就出现了上述情况:事务B正在读取数据,事务C就将数据修改了
要解决上述问题,就需要给读加锁,约定读的时候不能写
幻读
在 对写加锁 和 对读加锁 的前提下
事务A 修改了数据,提交,事务B开始读取
此时事务C新增(或删除)了一个其他数据
事务B就可能出现两次读取的 结果集(查询出来的行数)不同
要解决幻读问题,就需要让事务A、B 和 C 串行执行,即不再有任何并发,第一个事务执行完毕,再执行第二个任务
对于上述可能出现的 脏读、不可重复读 和 幻读,并不是一定要解决的BUG,而是要根据具体的需求,看当前更关注于数据的准确性,还是更关注于效率
因此,MySQL 在配置中,提供了 隔离级别 这样的选项,我们就可以根据需求,调整隔离级别,适应不同的情况
隔离级别 | 说明 |
---|---|
READ UNCOMMITTED | 读未提交,并行程度最高,隔离程度最低;效率最高,数据最不可靠,可能会出现 脏读、不可重复读 和 幻读 的情况 |
READ COMMITTED | 读已提交,给写加锁,并行程度降低,隔离程度提高;效率提高,数据可靠性提高,可能会出现 不可重复读 和 幻读 的情况 |
REPEATABLE READ | 可重复读,给读操作和写操作都加锁,并行程度更低,隔离级别更高;效率更高,数据更可靠,可能会出现 幻读 的情况 |
SERIALIZABLE | 串行化,让所有的事务都串行执行,并行程度最低,隔离级别最高;效率最低,数据最可靠 |
数据库默认的隔离级别为 REPEATABLE READ(可重复读)
事务的使用
事务的操作主要有:
1. 开启事务 start transaction(一组操作前开启事务)
2. 提交事务 commit(这组操作全部成功,提交事务)
3. 回滚事务 rollback(这组操作中间任意一个操作出现异常,回滚事务)
即:
开启事务
执行 SQL 语句
回滚或提交
开启事务:
start transaction;
提交:
commit;
回滚:
rollback;
例如:
start transaction;
update account set balance = balance - 2000 where name = 'A';
update account set balance = balance + 2000 where name = 'B';
commit;
事务的回滚一般不会在控制台使用,而是在 Java 代码中,控制事务的开启,接着执行 SQL 语句,在某个 SQL 语句抛出异常时,在 catch 语句中,捕获到异常,使用 rollback,进行回滚
这种情况下,是我们的程序出现了异常,而不是数据库出现了问题