事务与隔离级别
事务并发问题
多事务执行问题
业务系统并不是单线程运行,而是需要多个线程来同时响应多个用户同时发起的请求。每个线程中的事务,都会执行从磁盘加载数据页到内存缓存页,再由后台I/O
进程写回磁盘的过程,这需要解决几个问题。
如果多个事务同时对某一个缓存页里的数据进行更新,如何处理冲突?
一个事务在更新,另一个事务在查询,如何处理冲突?
MySQL只有InnoDB
引擎支持事务。

脏写(第一类丢失更新)
脏写
的定义是:事务A
和事务B
修改了同一个值,一个完成提交,一个异常回滚。不管哪个事务回滚,另一个事务所更新的数据就会丢失,这就是脏写
。
事务A
和事务B
同时更新某条数据X,事务A
将之更新为Y。事务A
更新后但未提交,事务B
紧接着更新为Z(事务B
提交或未提交不影响结果)。事务A
异常回滚,则该行数据恢复到X,造成事务B
更新的数据Z丢失。

经典的脏写
案例。
时序 | 存款事务 | 取款事务 |
---|---|---|
T1 | 查询账户余额1000 | |
T2 | 查询账户余额1000 | |
T3 | 开始事务 | |
T4 | 开始事务 | |
T5 | 存入100元 | |
T6 | 改余额为1100 | |
T7 | 取出100 | |
T8 | 改余额为900 | |
T9 | 提交事务 | |
T10 | 事务回滚 | |
T11 | undo log,账户余额回滚到1000 | |
T12 | 查询账户余额1000(数据900丢失) |
丢失更新(第二类丢失更新)
丢失更新
的定义是:两个事务并发更新同一条数据记录,后完成的事务造成先完成的事务更新丢失了。
事务A
和事务B
查询得到数据X。事务A
提交并更新,结果变为Y。事务B
提交并更新,结果变为Z。事务A
的更新丢失了。

经典的丢失更新
案例。
时序 | 存款事务 | 取款事务 |
---|---|---|
T1 | 查询账户余额1000 | |
T2 | 查询账户余额1000 | |
T3 | 开始事务 | |
T4 | 开始事务 | |
T5 | 存入100元 | |
T6 | 改余额为1100 | |
T7 | 提交事务 | |
T8 | 取出100 | |
T9 | 改余额为900 | |
T10 | 提交事务 | |
T11 | 查询账户余额900(数据1100丢失) |
脏读
脏读
的定义是:事务B
查询了事务A
修改过的值,但此时事务A
还未提交。当事务A
回滚后,事务B
就无法再次查询到刚才修改过的数据,这就是脏读
。
事务A
更新了某行数据X,更新后的数据为Y,但事务A
还未提交。事务B
查询到该行数据的值为Y并进行业务处理。事务A
异常回滚,当事务B
再次查询该行数据时,发现值变成了X,发生了脏读。

经典的脏读
案例。
时序 | 存款事务 | 取款事务 |
---|---|---|
T1 | 查询账户余额1000 | |
T2 | 开始事务 | |
T3 | 存入100元 | |
T4 | 改余额为1100 | |
T5 | 查询账户余额1100(脏读数据) | |
T6 | 回滚事务 | |
T7 | 账户余额恢复为1000 | |
T8 | 查询账户余额1000 |
不可重复读
不可重复读
的的定义是:同一个事务在多次查询相同数据的时候,得到的数值不同。
事务A
、事务B
和事务C同时处理某一行数据X,事务A
第一次查询,得到数据值X。事务B
修改该行数据值为B并提交,事务A
再次查询该行,得到数据值Y。事务C修改该行数据值为C并提交,
事务A
再次查询该行,得到数据值Z。事务A
希望多次查询得到的都是同一个值,即数据值是可以重复读的。但经过其他事务的修改并提交后,
事务A
得到数据明显是不可重复读的。
另外,不可重复读取决于具体场景,如果允许它存在,那就不是问题。

经典的不可重复读
案例。
时序 | 存款事务 | 取款事务 | 转账事务 |
---|---|---|---|
T1 | 查询账户余额1000 | ||
T2 | 开始事务 | ||
T3 | 存入100元 | ||
T4 | 改余额为1100 | ||
T5 | 提交事务 | 开始事务 | |
T6 | 查询账户余额1100 | 汇入100元 | |
T7 | 改余额为1200 | ||
T8 | 提交事务 | ||
T9 | 查询账户余额1200 |
幻读
幻读
的定义是:同一个事务用相同的SQL
多次查询,每次查询出来的数据都比上次多一些。
事务A
按条件查询,得到10条数据。事务B
插入数据并提交。事务A
按相同条件查询,得到多于10条的数据,多出来的数据就是幻读。

经典的幻读
案例。
时序 | 查账事务 | 还款事务 |
---|---|---|
T1 | 查询到已还款账户10个 | |
T2 | 开始事务 | |
T3 | 批量更改账户状态 | |
T4 | 提交事务 | |
T5 | 查询到已还款账户15个 | |
T6 | 开始事务 | |
T7 | 批量更改账户状态 | |
T8 | 提交事务 | |
T9 | 查询到已还款账户30个 |
联系与区别
脏写
为第一类丢失更新
,并发更新同一数据记录时,因事务A
回滚,造成事务B
无法从undo log
里恢复数据。丢失更新
为第二类丢失更新
,并发更新同一数据记录时,后完成的事务数据覆盖了先完成的事务数据。脏读
针对的是两个事务:事务B
读取了事务A
未提交的数据。不可重复读
仅针对单个事务:事务A
每次读出的记录值不同,重点在于update
和delete
,针对单条记录值。幻读
仅针对单个事务:事务A
每次查询的记录数不同,重点在于insert
,针对多条记录值。
事务隔离机制
标准的隔离级别
读未提交(Read Uncommitted,简写RU)
:一个事务可以读取另一个事务未提交的数据,不允许出现脏写
,但会出现脏读
、不可重复读
和幻读
的事务并发问题,事务性最差,但性能最强。读已提交(Read Committed,简写RC)
:一个事务只能读取另一个事务已经提交的数据,避免了脏写
和脏读
,但性能比读未提交
差。可重复读(Repeatable Read,简写RR)
:同一个事务多次读取同一条数据,返回的结果是一样的,避免了脏写
、丢失更新
、脏读
和不可重复读
,性能比读已提交差。串行化(Serializable,简写S)
:所有事务串行执行,避免了所有的事务并发问题,性能最差。
隔离级别与并发问题对应
隔离级别 | 脏写 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|---|
读未提交RU | √ | × | × | × | × |
读已提交RC | √ | × | √ | × | × |
可重复读RR | √ | √ | √ | √ | × |
串行化SER | √ | √ | √ | √ | √ |
MySQL事务隔离级别
虽然MySQL默认的事务隔离级别是可重复读(Repeatable Read)
,但跟标准的SQL
事务隔离级别还有些不同,因为MySQL的RR事务隔离级别
把幻读
也避免了。
这都是得益于MySQL的MVCC
(Multi-Version Concurrency Control,多版本并发控制)机制。
可以通过命令修改MySQL默认的事务隔离级别。
> SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
在Spring的@Transactional
注解中,可以通过isolation
参数设置隔离级别,@Transactional(isolation = Isolation.DEFAULT)
的可选值如下。
Isolation.READ_UNCOMMITTED
。Isolation.READ_COMMITTED
。Isolation.REPEATABLE_READ
。Isolation.SERIALIZABLE
。
感谢支持
更多内容,请移步《超级个体》。