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
是自己,因此此时就能看到这条幻影数据了;