MySQL的四种隔离级别及实现原理
原文链接:https://blog.csdn.net/ln82799/article/details/121025004
1 Read Uncommitted(读未提交)
在RU级别下,所有的事务都可以看到其他未提交事务所修改的数据,也就是说,在这个隔离级别下会产生脏读和幻读的问题。该级别性能也并不比其他隔离级别好多少,因此很少实际使用。
- 在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;
2 Read Committed(提交读)
在RC级别下,一个事务开始执行后,只能看到其他已经提交的事务造成的修改, 不能看到未提交事务对数据的修改,解决了脏读的问题。可能发生不可重复读和幻读现象。很多DBMS默认隔离级别都是RC。(MySQL除外)
实现原理: MVCC多版本并发控制
在每一次进行快照读的时候,都会创建新的ReadView,因此可以读取其他事务提交后的数据
3 Repeatable Read(可重复读)
在RR级别下,保证同一个事务中多次读取同样的记录的结果是一致的。解决了不可重复读和幻读,可能发生幻读现象。
实现原理:MVCC和 Next-key Lock
快照读:当事务第一次对数据进行快照读时,MVCC会创建ReadView,之后的每一次快照读,都会根据之前创建的ReadView进行数据读取,不会读取到其他事务对数据的修改,因此解决了不可重复读和幻读。
当前读:当事务进行当前读时,会以Next-Key Lock的方式对读取到的数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是Record Lock和Gap Lock的组合。会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。那样当其他事务便不能对上锁的数据进行修改和插入,保证不会产生幻读!
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
- 举个具体例子,场景如下:
事务 A 执行了这条当前读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁+记录锁的组合)。
然后,事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。这就避免了由于事务 B 插入新记录而导致事务 A 发生幻读的现象。
- 举个具体例子,场景如下:
Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁。(读提交隔离级别,是没有间隙锁的,只有记录锁)。
第一个发生幻读现象的场景
以这张表作为例子:

整个发生幻读的时序图如下
第二个发生幻读现象的场景
除了上面这一种场景会发生幻读现象之外,还有下面这个场景也会发生幻读现象。
- T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
- T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
- T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。
可重复读是如何规避幻读的&又是为什么不能完全解决幻读(**)
重复读(下文简称RR)理论上是无法解决幻读的,但通过多版本并发控制(下文简称MVCC)、排它锁(下文简称X锁)、临键锁(Next-Key锁,下文简称NK锁)等去尽量地规避幻读。 一、如何规避幻读。 MVCC有两种读取方式:快照读、当前读。 1、如果只是查询,例如 SELECT * FROM table; 这就是快照读,因为MVCC暗含创建版本号(下文简称CV),删除版本号(下文简称DV),SQL语句暗含了WHERE cv<=当前版本号(即当前事务ID) AND (dv is null OR dv>当前版本号);后面事务插入的数据读不到,这样就不会产生幻读。 2、如果涉及增删改,就使用当前读。读取最新的数据进行处理,即便是后面事务已提交的数据。例如事务1要更新记录A,但是事务2已经删除了记录A,这就会出问题。 由上可见,当前读是避免不了幻读的。因此加入了NK锁来规避。 假设有字段a=10、20、40、60、70几条记录。 事务1: SELECT * FROM table WHERE a=40 FOR UPDATE 这时候InnoDB锁的不仅仅是40这条记录,还会锁(20,40]、(40,60]这两个间隙(GAP)。 事务2,如果INSERT 11或者61,都能成功,但是如果INSERT 21、41就会失败。 所以通过NK锁的排他性,既保证了相关可能产生幻读的记录进不来,也保证了与之无关的数据可以INSERT,提高并发。
二、无法完全避免幻读。 设有如下记录,字段为: id name age cv dv 1 A 10 10 - 2 B 10 10 - 事务100: SELECT * FROM table WHERE age=10; 查询结果为2条记录。 事务200: INSERT INTO table VALUES (3,C,10); 此时表数据变为: id name age cv dv 1 A 10 10 - 2 B 10 10 - 3 C 10 200 - 事务100: SELECT * FROM table WHERE age=10; 查询结果仍为2条记录。因为这两次查询都是快照读。 事务100:不加任何条件,将表所有name改为D, UPDATE table SET name='D'; 因为是当前读,所以修改会作用在所有事务提交的数据上,包括事务200新增的数据3,此时表数据变成了: id name age cv dv 1 A 10 10 100 2 B 10 10 100 3 C 10 200 100 1 D 10 100 - 2 D 10 100 - 3 D 10 100 - 事务100: SELECT * FROM table WHERE age=10; 查询结果为3条记录,出现了幻读。 同理,事务100本来读的是2条,事务100更新之后,也会读出来3条。 事务100前两个SELECT都是快照读,无锁,事务200可以插入,但UPDATE之后,或者假如两个SELECT加了FOR UPDATE,有了NK锁,事务200就无法插入了。
小结
MySQL InnoDB 引擎的可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。
- 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。
两个发生幻读场景的例子。
第一个例子:对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。
第二个例子:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。
所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
2020-06-24 状态模式