文章目录
插播面试题
- MySQL中事务隔离级别有哪几种?
- 事务隔离级别的底层实现原理是什么?
问题一可能大家都有所了解,事务隔离级别嘛,不就是读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)、串行化(serializable)这几种嘛
这时候面试官可能又会问:这几种隔离级别分别解决了什么问题,又有什么缺点?
到这里没深入看过隔离级别原理或者自己没写过Demo的小伙伴可能就蒙蔽了,可能脑海中隐约有不可重复读、幻读这几个概念,但是一片混乱
针对问题二,可能就更蒙蔽了,我怎么知道隔离级别底层是怎么实现?难倒是加锁?
要解答这几个问题,还是得先从有哪些事务隔离级别说起
事务隔离级别
MySQL为了保证数据操作的一致性,因此有事务这么一个概念,同一个事务中所有的增删改操作,要么一起成功,要么一起失败。
事务隔离性是针对多个事务同时访问同一份数据而提出的概念,理论上在某个事务对数据进行访问、修改时,若其他事务也想访问这份数据,则应该进行排队,当之前一个事务提交之后,其他事务才可以继续访问这个数据。也就是传统意义上的串行化排队等待。但是我们知道串行是一种性能及其低下的方案,如果其他事务只想读取数据呢?串行显然不是一种最优方案,MySQL为了提升并发度,权衡性能和数据安全性,提出了各种隔离级别
下面以实例说明,假设有如下User表,表中初始化内容如下,2条用户数据
id | name |
---|---|
1 | name1 |
2 | name2 |
读未提交(read uncommitted)
顾名思义:B事务能够读取到A事务未提交的数据,即为读未提交
如图所示,假设A、B两个事务执行顺序如下,A事务执行过程中修改了name1的值为name3,但是还没提交,事务B就已经能读取到name3的值了
时间发生顺序 | 事务A | 事务B |
---|---|---|
1 | start transaction | start transaction |
2 | select name from user where id = 1(读取到的是name1) | |
3 | update user set name1=‘name3’ where id = 1(还没提交) | |
4 | select name from user where id = 1(读取到的是name3) |
读未提交是不符合大部分业务场景需求的,我都没提交的数据,被你读取到了,万一A事务此时做了回滚操作,但是B事务已经取到了修改后的值,那就违背了一致性的原则。
同时事务读取到了一个无效的值(可能被回滚了)的情况即称作脏读
平时使用中,需避免使用读未提交隔离级别
读已提交(read committed)
如果B事务每次都只能读取到A事务提交(Commit)后的数据,那么脏读问题也就解决了,同时这种隔离级别称为读已提交
同样的还是以表格来说明:事务B在事务A执行commit操作之前是无法读取到name3的值的
时间发生顺序 | 事务A | 事务B |
---|---|---|
1 | start transaction | start transaction |
2 | select name from user where id = 1(读取到的是name1) | |
3 | update user set name1=‘name3’ where id = 1 | |
4 | select name from user where id = 1(读取到的还是name1) | |
5 | commit | |
6 | select name from user where id = 1(读取到的是name3) |
这种隔离级别又有什么问题呢?我们发现只要其他事务修改并提交了数据,则B事务中始终能查询到修改后的值,也就是查询同一条数据时,同一事务的前后两个时间点中查询到的数据值不一致(其他事务进行了修改、提交)
这种在同一个事务中的不同时间点查询同一个数据,但是数据不一致的情况称为不可重复读
可重复读(repeatable read)
一个事务只能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。那么这种隔离级别就称之为可重复读
如图所示,在时间节点6时,A事务已经提交,但是B事务还是读取到的是name1,只有在B事务提交后再查询,才能查询到最新的值
时间发生顺序 | 事务A | 事务B |
---|---|---|
1 | start transaction | start transaction |
2 | select name from user where id = 1(读取到的是name1) | |
3 | update user set name1=‘name3’ where id = 1 | |
4 | select name from user where id = 1(读取到的还是name1) | |
5 | commit | |
6 | select name from user where id = 1(读取到还是name1) | |
7 | commit | |
8 | select name from user where id = 1(读取到是name3) |
同一事务中始终读到的是事务开始时数据的状态,即为可重复读
幻读的错误理解
可重复读隔离级别下又有什么问题呢?很多人网上搜可重复读隔离级别下有什么问题时,可能都会得到答案:可重复读下存在幻读的问题
并且给出了幻读的例子如下:假设User表中目前有2条数据,A、B同时开启事物,A向User中插入一条数据,则B事务中在2个时间节点查询到User表中数据不一致(后面查询多了一条数据),则称为幻读,用表格来说明的话即为如下步骤
时间发生顺序 | 事务A | 事务B |
---|---|---|
1 | start transaction | start transaction |
2 | select * from user(读取到1、2两个用户,没用户3) | |
3 | select * from user (读取到的也是用户1、2两个用户) | |
4 | 前面没查到用户3,执行插入操作 INSERT INTO . user (id , name ) VALUES (3, ‘name3’) | |
5 | commit | |
6 | select * from user (读取到的是用户1、2、3三个用户,多了一个用户) |
总的来说就是一句话:即可重复读操作不能阻塞或避免其他事务的插入insert、删除delete,其他事务插入或删除数据后,当前事务查询结果与前一次查询结果不一致
仔细想一想网上这种说法对不对?我个人认为前半句对,后半句不对
-
虽然MVCC机制(快照读取数据历史版本)能保证前后读取到一致的数据(可重复读),但不能阻塞其他事务的插入、删除操作
-
MySQL中InnoDb引擎下由于存在MVCC机制(下篇文章再做介绍),在RR隔离级别下,不管你怎么读,同一事务中都是读取到一致的数据的,因此不存在前后读取到数据不一致的情况,如果前后数据读取到的数据不一致,这是否也可以算不可重复读的一种呢,而并不是真正的幻读
幻读的正确理解
考虑如下场景,A、B两个事务同时要进行插入User3(id=3,name=3)的操作,正常逻辑就是判断这个用户是否存在,不存在则进行插入,执行顺序如下
时间发生顺序 | 事务A | 事务B |
---|---|---|
1 | start transaction | start transaction |
2 | select * from user(读取到1、2两个用户,没用户3) | |
3 | select * from user (读取到的也是用户1、2两个用户) | |
4 | 前面没查到用户3,执行插入操作 INSERT INTO . user (id , name ) VALUES (3, ‘name3’) | |
5 | commit | |
6 | 前面没查到用户3,执行插入操作 INSERT INTO . user (id , name ) VALUES (3, ‘name3’) 此时会抛异常,主键重复Duplicate entry ‘3’ for key ‘PRIMARY’ |
即B事务中,根据之前时间节点3的查询条件来判断用户3不存在,因此产生幻觉“认为”数据库中不存在这个用户,此时发起插入操作,发现报错(实际库中已经存在User3)
幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读,即前一次查询给了用户一种仿佛用户3不存在的幻觉
行锁、间隙锁
MySQL中为了避免幻读,引入了锁的概念(行锁、间隙锁、Next-key Lock),什么意思呢?
如果B事务中第一次查询时就加上间隙锁(防止其他事务进行插入),则A事务就不能进行插入操作,则B事务后续的插入用户操作不会报错(没有出现幻读)
时间发生顺序 | 事务A | 事务B |
---|---|---|
1 | start transaction | start transaction |
2 | select * from user(读取到1、2两个用户,没用户3) | |
3 | select * from user where id = 3 for update (读取到的也是用户1、2两个用户) | |
4 | INSERT INTO . user (id , name ) VALUES (3, ‘name3’)阻塞等到B事务提交 | |
5 | 前面没查到用户3,执行插入操作 INSERT INTO . user (id , name ) VALUES (3, ‘name3’) 插入成功 | |
6 | commit | |
7 | 1062 - Duplicate entry ‘3’ for key ‘PRIMARY’ |
如图时间点3中,B事务查询操作中加入for update操作,由于id=3的记录不存在,因此加的是间隙锁,此时A事务执行INSERT语句时会阻塞,直到B事务提交,因此B事务中后续插入成功、没有出现幻读。关于锁的更多内容、本文不再介绍。
看到这里,大家可能就明白了为什么RR级别是MySQL下默认的隔离级别,它是一种性能和安全性考虑的折中方案,虽然存在幻读的情况,但是可以通过人为加锁控制解决幻读的情况
串行化(serializable)
串行化即为传统意义上的排队等待,所有事务串行执行,需要等待上一个事务结束,当前事务才能得以执行,即使当前事务中进行的读操作。
这种隔离级别拥有最高的安全性,但性能上也损失严重。
看到这里文章开头第一个问题MySQL中事务隔离级别有哪几种? 相比各位已经有了答案了。还剩第二个问题
事务隔离级别的底层实现原理是什么?
这里说的事务隔离级别的底层实现原理,主要针对的就是读已提交和可重复读隔离级别,其实关键点就是2个
- 读已提交中隔离级别下,MySQL是怎么保证每次查询都读取到最新提交的数据的,却查询不到未提交的数据
- 可重复读隔离级别下,MySQL又是怎么使得事务中后续的每次查询都和第一次保持一致的?
下篇文章中会对读已提交、可重复读中的快照读原理(MVCC)做详细介绍