一、MVCC多版本并发控制机制
- mysql在读已提交和可重复读的隔离级别下,通过MVCC机制实现的。
二、undo日志版本链
- undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链
三、read view机制详解
- 当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束
之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
四、实例
假如数据表test中有一条id为1的数据,name值为1,是由事务1插入完成的。然后开启如下事务:
开启事务如下100、200、300,依次按照从上往下的时间执行:
时间 | # Transaction 100 | # Transaction 200 | # Transaction 300 | # select 1 |
---|---|---|---|---|
1 | begin; | begin; | begin; | begin; |
2 | update test set name = ‘2_100’ where id =3; | |||
3 | update test set name = ‘3_200’ where id =2; | |||
4 | update test set name = ‘4_300’ where id = 1; | |||
5 | commit; | |||
6 | select name from test where id = 1; | |||
7 | update test set name = ‘9_100’ where id = 1; | |||
8 | select name from test where id = 1; | |||
9 | commit; | |||
10 | update test set name = ‘12_300’ where id = 1; | |||
11 | select name from test where id = 1; | |||
12 | commit; |
每一条数据都有一个undo日志版本链,这里只研究id为1的:
- 时间1时,都开启事务
- 时间2:Transaction 100事务,将id为3的记录name值修改为2_100
- 时间3:Transaction 200事务,将id为2的记录修改为3_200
- 时间4:Transaction 300事务,将id为1的记录修改为4_300,原本表中的值为1。(id,name,trx_id,roll_pointer)其中id代表记录ID,name就是这次修改的字段,trx_id则是事务id,roll_pointer指向下一个修改记录。则当前生成的undo日志版本链,可大致表示为:(1,1,1,)–>(1,4_300,300,)
- 时间5:提交Transaction 300事务。
- 时间6:# select 1对id为1的记录进行查询。根据上面read-view生成规则(所有未提交事务id数组和已创建的最大事务id组成)。此时未提交的事务为Transaction 100、Transaction 200,组成的数组可记为[100,200],已创建的最大事务id为300.则当前read-view为[100,200],300
- 时间7:Transaction 100事务,将id为1的记录name值修改为9_100,则undo日志版本链变为(1,1,1,)–> (1,4_300,300,) -->(1,9_100,100,)
- 时间8:# select 1对id为1的记录进行查询。则当前的read-view为[100,200],300
- 时间9:提交Transaction 100事务。
- 时间10: Transaction 200事务,将id为1的记录name值修改为12_300,则undo日志版本链变为(1,1,1,)–> (1,4_300,300,) -->(1,9_100,100,)–>(1,12_300,200,)
- 时间11:# select 1对id为1的记录进行查询。则当前的read-view为[200],300
- 时间12:提交Transaction 200事务。
版本链比对规则:
- 如果 row 的 trx_id 落在( trx_id<min_id )部分,表示这个版本是已提交的事务生成的,这个数据是可见的;
- 如果 row 的 trx_id 落在( trx_id>max_id )部分,表示这个版本是由将来启动的事务生成的,是不可见的(若row 的 trx_id 就是当前自己的事务是可见的);
- 如果 row 的 trx_id 落在部分(min_id <=trx_id<= max_id),那就包括两种情况
a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
1.当开启读已提交隔离级别
- 时间6:read-view为[100,200],300,其undo日志链为(1,1,1,)–>(1,4_300,300,);当前undo日志记录的事务id为300,满足上面版本链对比规则第三条中的b,则代表可见。所以此刻查询出来的值为4_300
- 时间8:read-view为[100,200],300,其undo日志链为(1,1,1,)–> (1,4_300,300,) -->(1,9_100,100,);当前的事务id为100,满足上面版本链对比规则第三条中的a,则代表不可见。此时就要回退一个版本链到 (1,4_300,300,),上步已做分析可见。所以此刻查询出来的值为4_300
- 时间11:read-view为[200],300,其undo日志链为(1,1,1,)–> (1,4_300,300,) -->(1,9_100,100,)–>(1,12_300,200,),满足上面版本链对比规则第三条中的a,则代表不可见。此时就要回退一个版本链到 (1,9_100,100,),满足上面版本对比规则第一条,代表可见。则读取的数据为9_100
2.当开启可重复读隔离级别
开启可重复读,与读已提交的对比规则一致。但是,在可重复读的隔离级别下,执行第一条查询语句生成read-view后,后面不再改变。
如上面实例:
- 在时间6:生成read-view为[100,200],300后,时间8和时间11的read-view仍然为[100,200],300。
- 剩下的比对规则跟上面一致。如此也就保证可重复读。