前言
数据库的事务已经有很多人讲过了,网上一搜一大堆,但想着自己写一遍的话,印象能够更加深刻,会有更深的理解。
什么是事务?
在说起事务时,总是离不开一个例子:银行转账。假设现在有这么一个场景:
假设⼀个银⾏的数据库有两张表:⽀票(checking)表和储蓄(savings)表。现在要从Jane的⽀票账户移200元到她的储蓄账户。
那么需要以下三步:
- 检查支票账户中的余额大于等于200元
- 从支票账户中减去200元
- 在储蓄账户中增加200元
SELECT balance FROM checking WHERE name='Jane';
UPDATE checking SET balance = balance - 200.00 WHERE name='Jane';
UPDATE savings SET balance = balance + 200.00 WHERE name='Jane';
这三条sql必须全部执行,如果其中一条出了错,其它语句也必须撤销,比如第三条sql出现异常报错了,那么前面两条就必须撤销,这个操作叫回滚.
像这种把多条sql语句作为一个整体进行操作的功能,就叫做数据库事务。 数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库数据有任何改动。
事务的特性(ACID)
- A(Atomic):原子性,一个事务中的sql语句要不全部成功,要不全部失败。对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。
- C(Consistency):一致性,一个事务完成后,数据库中的数据的状态必须是一致的。
- I(Isolation):隔离性,一个事务在提交前,对另一个事务是不可见的,需要隔离开来的。
- D(Duration):持久性,事务一旦完成,对数据库的修改将被保存到数据库中进行持久化。
事务在并发操作下会产生的问题
- 对于两个并发执行的事务,如果操作的是同一个数据库中的同一条数据时,由于数据一致性的问题,会产生问题,例如脏读、幻读、不可重复读。
- 下面我们还是用转账来说明一下这些问题
脏读
两个事务在并发执行时,A事务在执行过程中读取了B事务中修改了但未提交的数据,这就是脏读。
时间 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | - |
2 | - | 开始事务 |
3 | - | 查询余额:1000元 |
4 | - | 取出1000元,余额为0 |
5 | 查询余额:0元 | - |
6 | - | 出现异常,事务回滚 |
7 | 存入500元,余额500元 | - |
8 | 提交事务 | - |
从上面的表格我们可以看到,在时间 5 的时候,事务A查询到的余额是0元,是事务B进行取钱以后的数据,在时间 6 的时候,事务B遇到了问题,回滚了事务,但事务A并不知道,于是事务A继续他的操作,存入500元,此时余额是500元,事务提交。随着事务A的提交,那么数据库中余额就变成了500元,但实际上,由于事务B执行失败了,并没有取出这1000元,事务A存500元后,余额应该为1500元才对,而这就是事务的脏读。
不可重复读
两个事务并发执行时,事务A在读取同一条数据且中间没有进行修改时获取到的结果并不相同,这是因为在事务A执行的中途,有其它事务修改了这条数据并提交了事务,所以导致事务A读取到了不同的数据,这就是不可重复读。
时间 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | - |
2 | - | 开始事务 |
3 | - | 查询余额:1000元 |
4 | 查询余额:1000元 | - |
5 | - | 取出1000元,余额0元 |
6 | - | 提交事务 |
7 | 再次查询余额:0元 | - |
8 | 提交事务 | - |
从上面的表格我们可以看到,事务A在时间 4 和时间 7 查询到的账户余额是不同的,这是因为事务B在时间 5 的时候取出了1000元并且在时间 6 提交了事务,也就是说不可重复读就是读取到了其它事务已经提交的数据。(一些场景绝不允许,例如提现)
幻读
两个事务并发执行时,事务A统计的结果和第二次统计的结果不一样,是因为事务B新增或者删除了一条数据,和不可重复读一样,都是读到了其它事务已提交的数据。不同的是,不可重复读读的是同一条数据,而幻读读到的是一批数据,或者说可重复读是事务A读取了事务B更新的数据,而幻读是事务A读取了事务B新增或删除的数据。
时间 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | - |
2 | - | 开始事务 |
3 | 查询用户转账记录:10条 | - |
4 | - | 转账1000元 |
5 | - | 添加转账记录 |
6 | - | 提交事务 |
7 | 查询用户转账记录:11条 | - |
8 | 提交事务 | - |
从上面的表格我们可以看到,事务A在时间 3 和时间 7 查询到的转账记录的条数是不同的,这是因为事务B在时间 4 又发生了一次转账,并在时间 5 添加了一条转账记录,也就是说,幻读读取的是一个表的数据的条数不同,而不可重复读读的是一条数据的数据内容。
特别注意:在这种情况下,事务A的查询需要在后面加上for upadate,使用当前读,否则使用的就是快照读,读取不了事务B的insert操作,统计结果还是10条
事务的隔离级别
读未提交(Read Uncommitted):
在Read Uncommitted级别,事务中的修改,即使没有提交,对其它事务也是可见的。事务可以读取未提交的数据,这也叫脏读(Dirty Read)。
读已提交(Read Committed)也叫不可重复读。
一个事务从开始到提交之前,所做的任何修改对其它事务都是不可见的。解决了脏读的问题。 一般的数据库默认的隔离级别,如Sql Server、Oracle
可重复读(Repearable Read)
开始读取数据时(事务开启),就不允许再进行修改操作。解决了不可重复读的问题。 Mysql数据库默认的隔离级别。
可串行化(Serializable)
可串行化是最高的事务隔离级别,在该级别下,事务串行化执行。解决了幻读的问题。 几乎不用,因为比较消耗数据库性能。
隔离级别解决的对应问题
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | - | - | - |
读已提交 | 解决 | - | - |
可重复读 | 解决 | 解决 | - |
串行化 | 解决 | 解决 | 解决 |
以上。