MVCC多版本并发控制
- 目的:主要是为了提高数据库并发性能,更好的处理在并发事物中读-写产生的并发冲突;避免使用锁解决并发冲突,保证读操作在任何时候都是非阻塞的;
原理解析
实现机制
主要通过隐藏字段、undo-log日志、ReadView读视图实现;
-
隐藏字段(
DB_ROWID、DB_DELETED_BIT、DB_TRXI_ID、DB_ROLL_PTR):-
ROW_ID隐藏主键(6Bytes):当不存在一个唯一且非空属性的字段时,会隐式定义一个顺序递增ROW_ID来作为聚簇索引的索引列; -
DELETE_BIT删除标识(1Bytes):对于delete语句,当执行sql后并不会立马删除这条数据,而是将DELETE_BIT删除标识改为1,后续sql在检索到这条数据时,不会将DELETE_BIT = 1的数据纳入结果集;之后由Mysql的
purger线程自动清理DELETE_BIT = 1的数据(purger线程自身也会维护一个ReadView,只会删除DELETE_BIT = 1且TRX_ID对ReadView可见的数据,避免对MVCC的工作产生影响);优势:对聚簇索引而言,当事物在删除一条数据后(可能出现节点合并的情况),后续又执行了回滚操作(又插入了一条数据1,可能导致节点分裂),可能存在两次对索引结构的调整;
-
TRX_ID最近更新事物的ID(6Bytes):Mysq对于所有包含写入的事物都会分配一个顺序递增的事物ID,若是select语句则事物ID=0;TRX_ID就是记录最近一次改动当前这条数据的事务ID; -
ROLL_PTR回滚指针(7Bytes):当事物对一条数据做了改动后,会在undo-log中插入一条就版本的数据记录,而ROLL_PTR就是这个记录的地址指针,当需要回滚事物时,可以通过这个地址来回滚找到之前的旧版本数据;
-
-
undo-log日志:- 存储旧版本的数据,对于某一条数据它会构建出一个通过
ROLL_PTR回滚指针作为连接点的单向链表; update语句时的过程:- 对修改行的数据加上写锁;
- 将原本的旧数据拷贝到
undo-log和rollback segment区域; - 对表数据进行修改,完成后将
trx_id改为当前事物ID; - 将
ROLL_PRT指向undo-log中对应的旧数据,并在提交事物后释放锁;
- 优势:方便实现事物点回滚;实现MVCC机制;
- 清除:与
delete清除类似;
- 存储旧版本的数据,对于某一条数据它会构建出一个通过
-
ReadView读视图:
-
一个事务在尝试读取一条数据时,基于当前MySQL的运行状态生成的快照;
-
当一个事物启动后,首次执行
select操作时,MVCC会生成数据库的当前ReadView,一般包含:creator_trx_id:创建这个ReadView的事物ID;trx_ids:在创建这个ReadView时,系统内活跃的事物ID列表(未结束的事物);up_limit_id:活跃事物列表中,最小的事物ID;low_limit_id:表示在生成当前ReadView时,系统中要给下一个事务分配的ID值;
-
假设目前数据库中共有T1~T5这五个事务,T1、T2、T4还在执行,T3已经回滚,T5已经提交,此时当有一条查询语句执行时,会生成一个快照的信息如下:

{ "creator_trx_id" : "0", "trx_ids" : "[1,2,4]", "up_limit_id" : "1", "low_limit_id" : "6" }
-
实现原理
-
读视图的生成:当事物在执行查询语句时,就先去获取行数据的隐藏列,然后过判断后,可以获得目前查询事物的日志可以获得哪一个版本的事物,如果可以获得最新的则返回表中数据,如果不能则去
undo-log中获取旧版本数据返回; -
最新数据访问判断:例子:假设目前存在两个线程T1和T2;
-- T1: trx_id = 1 update test set a = "111" where id = 1; update test set b = "222" where id = 1; -- T2: trx_id = 2 select * from test where id = 1;- 当【T2】执行
select语句时,会生成一个ReadView; - 判断【T2】行数据中的
trx_id是否等于【T2】的creator_trx_id也就是事物Id;(读写同事物)- 相同:说明修改数据行的事物【T1】与创建读视图的事物【T2】时同一个,可以获得最新数据;
- 不相同:则说明数据被其他事物修改过;
- 比较
trx_id和up_limit_id最小活跃事物ID;(读时写是否提交)- 小于:说明修改事物【T1】在【T2】读视图创建前就已经提交,可以获得最新数据;
- 不小于:说明修改事物【T1】还在执行;
- 比较
trx_id和low_limit_id;(读时写是否创建)- 大于或等于:说明修改事物【T1】是【T2】创建读视图之后开始的,不能访问最新数据;
- 小于:需进一步判断,判断
trx_id是否在trx_ids中:- 在:说明需要改动的事物还在执行【T1】,不能访问最新数据;
- 不在:说明需要改动的事物执行结束了【T1】,可以访问最新数据;
- 当【T2】执行
-
旧版本数据访问判断:根据隐藏列
roll_ptr找到链表头,然后遍历寻找旧版本数据的trx_id不存在trx_ids活跃事物列表中数据; -
新增情况分析(幻读):
- 当事物T1在查询数据时,事物T2突然新增一条数据,此时T2的
trx_id会存在于trx_ids中,所以需要去查询旧版本数据,但因为是新增操作,因此回滚指针ROLL_PTR=null,则表明新旧版本数据都无法得到,这条数据对T1事物不可见;
- 当事物T1在查询数据时,事物T2突然新增一条数据,此时T2的
RC、RR隔离级别下的MVCC机制
Read Committed读已提交级别:- 会在每次
select语句执行前,生产一个ReadView读视图;存在不可重复读问题;
- 会在每次
Repeatable Read可重复读级别:- 会在每个事物第一次执行
select语句时生成ReadView,后续在出现select操作不会生成新的ReadView;解决了不可重复读问题;
- 会在每个事物第一次执行
Repeatable Read可重复读级别下的幻读问题:
-
对于上述4可以得知,mysql的MVCC机制已经在RR级别下解决了幻读问题,但在极端情况下还是可能出现
-
极端场景:假设有两个事物T1、T2,假设表test有3条数据,ID分别为1、2、3;
事物T1通过
select * from test where id > 2,查询id>2数据,此时事物T2通过insert into test values(5, '名称'),插入一条id=5的数据并提交,此时根据上述3.iv可知ID=5这条数据对T1不可见,但若此时事物T1执行update test set name = '名称T1' where ID = 5之后再次执行select * from test where id > 2此时能查询到id = 5这条记录了,这是因为MVCC通过快照去检索数据时,会发现Id = 5这条数据的trx_id是自己,因此此时就能看到这条幻影数据了;
-
本文详细解析了MVCC(多版本并发控制)在提高数据库并发性能、处理并发冲突和读写一致性中的作用,涉及隐藏字段、undo-log日志、ReadView的实现机制以及在不同隔离级别下的工作原理,特别强调了幻读问题的处理。
220

被折叠的 条评论
为什么被折叠?



