1.隔离级别基础理论
1.1 什么是事务的隔离级别
sql标准定义了4种隔离级别,每种隔离级别都有自己的规则,用来限定事务内外改变的可见性,以及高并发处理事务的执行效率,ISOLATION_SERIALIZABLE(可串行化)、ISOLATION_REPEATABLE_READ(可重读)、ISOLATION_READ_COMMITTED(读取提交内容)、ISOLATION_READ_UNCOMMITTED(读取未提交内容)。级别越高越安全,并发执行效率越低。
1.2 事务的四种隔离级别
- ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。 这种隔离级别会产生脏读,不可重复读和幻像读。
- ISOLATION_READ_COMMITTED:保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
- ISOLATION_REPEATABLE_READ:通过MVCC多版本控制管理产生一个readview对象,同一个事务多次读取数据都是从readview对象中读取所以读取的数据都是一致的,这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。 它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免不可重复读的情况产生(这是mysql默认的隔离级别)。
- ISOLATION_SERIALIZABLE:相当于把并行执行的事务变成串行执行了,效率极低几乎不会使用,这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。 除了防止脏读,不可重复读外,还避免了幻像读。
1.3 事务在并发情况下产生的问题
- 脏读(Drity Read):事务A更新了数据库的一条数据但是未提交,事务B可以读取事务A未提交的数据,这就是脏读。
- 不可重复读(Non-repeatable read):在同一个事务中两次读取的数据不一致,比如事务A第一次读取账户表,事务B对账户表一条记录进行修改,事务A再一次读取账户表可以读取到事务B修改的那条数据,导致事务A两次读取的结果不一致。
- 幻读(Phantom Read):在一个事务的两次读取数据的数量对不上,例如有一个事务A查询账户表一共有3条数据,事务B却在此时插入了一条新数据,事务A第二次查询,就查询不到事务B新增的那条数据,如果此时事务A插入一条和事务B插入相容的数据,就会报错。此时对于事务A来说就像是幻觉一样,明明不存在的数据为啥就是插入不成功。(事务隔离级别为ISOLATION_REPEATABLE_READ下的幻读)
1.4 隔离级别和并发问题关联

2.隔离级别实践
2.1行锁
所谓行级锁是mysql InnoDB引擎为了防止高并发场景下同一个数据库中多线程读写一张表的同一条数据而引发一系列的并发问题。对于事务来说开启事务并且对表中的一条数据做了写操作就相当于是已经获取到了这行数据的行锁,在事务未提交之前行锁是不会被释放的。
步骤
使用navicat开启两个客户端,建立两个会话,每个会话都有自己的session,模仿高并发场景。

- 客户端1开启事务。
- 客户端1查询account表,此时可以看到表中有一条lilei的数据
- 客户端1更新账户表lilei的数据。
- 客户端2也更新了账户表中lilei的数据。
- 客户端2等待一段时间后然后报错锁等待超时。因为客户端1对lilei进行update的时候就已经绑定了lilei这条数据的行锁,客户端2对liei进行update时客户端1还未提交事务所以lilei这一行的行锁还在被客户端1的事务绑定。所以客户端2会等待客户端1释放行锁,一直等到获取锁超时然后就报错了。
- 客户端1提交事务,此时客户端1已经把lilei这一行的行锁释放了。
- 客户端2再一次update lilei这行数据然后就成功了。
2.2 脏读
脏读的理论此处不再赘述,看本篇文章的第一部分理论篇即可。此处主要通过mysql演示脏读出现的场景。
查看和设置客户端事务隔离级别的命令:
未提交读
set session transaction isolation level read uncommitted;
显示事务的隔离级别:
show variables like 'tx_isolation';
脏读演示步骤:

- 设置客户端2的事务隔离级别为未提交读,因为只有未提交读这种隔离级别才会存在脏读的场景。
- 客户端2查看事务的隔离级别,发现隔离级别已经改成未提交读了,这种直接通过navicat建立的会话设置事务隔离级别都是session级别的,只会对当前会话产生影响。
- 客户端2查询账户表,查询出lilei的账户余额是3900。
- 客户端1开启事务
- 客户端1查询账户表,查询出lilei的账户余额是3900。
- 客户端1更新lilei这行数据的余额,balance-500,更新完成后不提交事务。
- 客户端2查询账户表,发现lilei的余额已经变成3500了,客户端2读取到了客户端1已经修改但是未提交的数据,这就是脏读。
2.3 不可重复读
查看和设置客户端事务隔离级别的命令:
已提交读
set session transaction isolation level read committed;
显示事务的隔离级别:
show variables like 'tx_isolation';
不可重复读演示步骤:

- 设置客户端2的事务隔离级别是已提交读并且查看客户端2的事务隔离级别已经设置成功。
- 客户端2开启事务。
- 客户端2查询账户表,此时lilei的账户余额3400。
- 客户端1开启事务。
- 客户端1更新账户表但是不提交事务。
- 客户端2查询账户表,此时lilei的账户余额还是3400。
- 客户端1提交事务。
- 客户端2查询账户表,lilei的账户余额已经变成2900了,客户端2在同一个事务中多次查询账户表出现数据不一致问题这个就是不可重复读。
补充:
因为客户端2已经设置成读已提交了,所以客户端2必须要等到客户端1提交事务之后才能查询到lilei账户余额的变动。如果客户端2是读未提交,客户端1的事务还未提交时,客户端2就已经可以查询到lilei的账户余额发生变动了。
2.4 可重复读
查看和设置事务的隔离级别的命令:
可重复读
set session transaction isolation level repeatable read;
显示事务的隔离级别:
show variables like 'tx_isolation';
可重复读演示步骤:

事务隔离级别要先设置为可重复读(默认的隔离级别就是可重复读)
- 客户端1查询账户表可以看到lilei的账户余额是800,因为是可重复读所以在查询时已经做了快照(mvcc多版本并发控制)
- 客户端2对lilei的账户余额进行修改-50并且提交事务数据更新到数据库,这时数据库中lilei的实际余额应该是750;
- 客户端1再一次查询账户表可以看到lilei的账户余额还是800,因为第二次查询并不是直接从数据库中查询而是查询的缓存数据所以lilei的账户余额还是800(可重复读);
- 客户端2查询账户表可以看到lilei的账户余额已经变成750了;
- 客户端1对lilei的账户余额进行修改update -50
- 客户端1查询account表发现lilei的账户已经变成了700了而不是750,这是因为客户端1对数据库进行update操作时数据不再是缓存的数据而是从数据库中拿到数据然后再执行update操作所以查询出来的数据就是700,这个是基于mysql的mvcc机制。
2.5 幻读
查看和设置事务的隔离级别的命令:
可重复读
set session transaction isolation level repeatable read;
显示事务的隔离级别:
show variables like 'tx_isolation';
幻读演示步骤:

- 客户端1开启事务并且查询账户表,一共有三条数据。
- 客户端2开启事务并且往账户表中新增一条数据。
- 客户端2提交事务
- 客户端1查询账户表和第一次查询结果相同,因为事务的隔离级别是重复读,根据mvvc的原则第一次查询后会产生快照因为查询的是快照所以客户端2新增的数据查询不到。
- 客户端1更新账户表id=4的数据,结果更新成功并没有报错,因为update的时候不会使用快照数据,而是会直接对数据库中的数据进行修改所以会成功。
- 客户端1查询账户表,此时可以查询出客户端2新增的一条账户信息。
结论:重复读不能阻止幻读,如步骤5中客户端1尚未提交事务但是对客户端2新增的数据进行update的时候是成功的。所以可以得出结论事务隔离界别(重复读)不能组阻止读。
补充:
上面演示的幻读是隔离级别设置为可重复读的演示,如果隔离级别是读未提交或者读已提交的情况下,幻读则是客户端1开启事务第一次查询3条数据,客户端2插入一条新的记录,客户端1在同一个事务中第二次查询就变成4条数据了,客户端1第一次查询是3条数据,第二次查询变成了4条数据就像产生了幻觉一样。