"时光机"与"多维视界"⭐️MySQL中原子性与隔离性的科幻大片
上篇文章 我们描述完MySQL的持久性等知识点,本篇文章来描述MySQL的原子性与隔离性知识
"时光机"指的是实现原子性的undo log,"多维视界"指的是实现并发场景下读不加锁的MVCC,一起往下看看吧~
内容脑图如下:
MySQL中支持事务的只有Innodb,因此本篇文章描述的原理也是Innodb实现原子性的原理
事务的原子性:一组事务要么都成功,要么都失败
在事务中可能存在一些写操作(新增/修改/删除),有的写操作会执行成功,但是后续写操作执行失败就会导致事务需要回滚,那么执行成功的写操作就要退回到原来的"版本"
为了方便回滚,Innodb使用undo log来记录每次写操作修改的内容,根据undo log能够快速退回到原始版本
每一行记录中存在隐藏列 roll_pointer 回滚指针,它指向上一次写操作产生的undo log,undo log中也存在类似回滚指针的列,它能够找到它上一次写操作产生的undo log
记录与undo log就会形成一条"版本链",这样在遇到回滚时便能快速的回到原来的版本以此来实现原子性(回滚)
注意:undo log只是记录修改的数据,并不是完整数据,图中只是为了方便展示,图中执行SQL顺序为从下到上
隔离性
为了防止并发事务交叉执行导致的数据不一致等并发问题,MySQL会根据不同的隔离级别来解决不同的隔离性问题
隔离性问题与隔离级别
不同隔离级别下会使用不同的方案来达到隔离性,我们先来复习一下隔离性问题和隔离级别:
隔离性问题
脏写:A事务和B事务同时修改一行记录,A可以覆盖B修改后未提交的记录,后续B又进行回滚
脏读:A事务读取B事务未提交的记录,然后B事务回滚,导致A事务读取的数据不一致
不可重复读:A事务前后两次执行同一个查询,返回的结果数据不同,B事务在此期间进行修改
幻读:A事务前后两次执行同一个查询,返回的结果数量不同,B事务在此期间进行新增
幻读强调结果数量不同,不可重复读强调结果数据不同
隔离级别
读未提交(RU):可以读到其他事务未提交的记录,会出现脏读、不可重复读、幻读
读已提交(RC):只能读到其他事务提交的记录,会出现不可重复读、幻读
可重复读(RR):Innodb默认,不会出现不可重读的情况,可能出现部分幻读
串行化(S):所有隔离性问题都会不发生
随着隔离性的严格,性能也会降低:RU > RC > RR > S
MVCC
那么在undo log的版本链中是如何做到有的事务能够看到该版本、有的事务看不到该版本的呢?
其实这是通过MVCC(Multi Version Concurrency Control 多版本并发控制)来实现的,MVCC在这种读写并发的场景下,使用无锁机制读取到满足隔离级别的数据
对于事务来说,当事务中出现第一条写操作的语句时就会生成事务ID,这个事务ID是全局递增的
当使用MVCC时会生成read view来判断当前事务能够读到哪个版本上的记录
read view中就包含事务ID相关的属性,便于判断事务能读到哪个版本
- 最小事务ID:read view生成时活跃事务中最小的id
- 最大事务ID:read view生成时活跃事务中最大的id
- 活跃事务列表:read view生成时存在的活跃事务
前面说到:事务ID是全局自增的,这表示可以根据事务ID查看哪个事务先执行、哪个事务后执行、哪个事务已提交/回滚、哪个事务当前还在执行中
使用read view与版本链上的最新记录进行判断(判断规则如下):
- 如果read view的最小事务id大于该记录的事务id,说明该记录的事务已经提交了,那么是可以查看的
- 如果read view的最大事务id小于该记录的事务id,说明该记录的事务在当前事务开启后才开始,那么无法查看
- 如果该记录id在read view最小、大事务id之间,那么要查看当前活跃事务列表,如果在列表中,说明该列的事务活跃(还未结束),则无法查看;反之则可以查看
- 如果记录的事务id是当前事务,则可以查询(当前事务执行的,当然可以查看)
当该版本的记录无法查看时,就会遍历版本链继续向下一条记录使用该规则
比如read view 最小事务id:20 最大事务id:30 活跃事务列表:20,22,24,27,30
当记录的事务id为19时,小于最小事务id,说明它已经提交可以查看
当记录的事务id为31时,说明生成read view时,它还没开启事务,不能查看
当记录的事务id为24时,在最小19、最大30之间,要查看活跃事务列表中是否存在,存在则不能查看
当记录的事务id为23时,活跃事务列表中不存在则可以查看
分析读相关解决隔离性问题的方案
在了解隔离性问题与隔离级别后,我们来进行一一分析:
脏写发生在写写的场景下会破坏数据的一致性,禁止这种情况发生,会使用行锁保证(锁相关知识下篇文章再说)
脏读发生在读写的场景下会读的数据不一致,也要禁止这种情况发生,正常情况读写场景下需要保证数据一致性的办法就是加行锁互相阻塞
但RC使用MVCC机制,在进行读操作时使用read view查看版本链上的记录是否可读,如果事务已提交(事务id会比read view最小事务id要小),以此通过无锁机制达到防止脏读的效果
不可重复读侧重的是有其他事务在本次事务多次查询过程中修改这个数据的内容(幻读侧重数量)
RC下,由于每次读都生成read view,前后两次读生成的read view不同,能看到的数据也就可能不同
RR使用MVCC机制,在同个事务中只有第一次读生成read view,后续读都使用这个read view,以此来达到防止不可重复读,和大部分幻读
幻读案例
为啥说RR可以防止大部分幻读呢?
RR通过read view可以看到的数据是不变的,因此可以防止幻读
那为啥说是防止大部分幻读而不是所有幻读呢?
还记得第四条规则(如果记录的事务id是当前事务,则可以查询)吗?
在某些场景下就会导致特殊幻读,来看一个案例:
时间点 | T1 | T2 |
---|---|---|
1 | begin; SELECT * FROM s where id < 30; | |
2 | insert into s (id,s_name,s_age) value (18,'caicai',18); | |
3 | update s set s_name = '菜菜的后端私房菜' where id = 18; | |
4 | SELECT * FROM s where id < 30; commit; |
T1 先进行查询(-∞,30),T2在该区间插入一条记录(18),如果此时再进行读则数据还是一致的
但是T1对这条新增的记录进行修改,导致T1的read view对该事务可见,从而导致第二次读到这条T2新增的数据发生幻读
在S串行化下,如果是自动提交则只有一条读会使用mvcc,写则会加锁
至此,我们描述完读相关解决不同隔离性的方案,写相关解决不同隔离性的方案与锁相关,我把它放在下一篇文章和锁一起进行讲解
总结
记录的隐藏列中存在回滚指针和事务ID,回滚指针指向undo log,undo log又可以通过指针找到上一次修改产生的undo log,从而形成版本链(undo log只记录修改数据)
事务的原子性(需要回滚时)可以通过undo log实现
在并发读写场景下,Innodb的读操作通过mvcc来保证不同隔离性下数据一致性
mvcc使用生成read view、全局递增的事务ID和能够看到哪个版本的校验规则来实现
RU读时没使用mvcc,可以直接读到版本最新记录
RC每次读时生成read view,只能防止脏读
RR第一次读生成read view,可以防止脏读、不可重复读、大部分幻读
S只在自动提交时使用mvcc
最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 MySQL进阶之路,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 gitee-StudyJava、 github-StudyJava 感兴趣的同学可以stat下持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜