什么是MVCC?
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
什么是当前读和快照读?
当前读是指读取的永远是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读
快照读是指事务在启动的时候就“拍了一个快照”,快照的实现是基于每条数据都存在多个版本。快照读读到的不一定是最新的版本,可能会读到历史版本。不加锁的select操作就是快照读
MVCC的实现原理
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的字段:
- DB_TRX_ID
6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务 - DB_ROLL_PTR
7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里) - DB_ROW_ID
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引 - 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
undo log
InnoDB里边每行数据都会存在多个版本。每次事务更新数据的时候,都会生成一个新的数据版本,并且新的数据版本可以通过DB_ROLL_PTR找到旧的数据版本。
如图所示:就是一个记录被多个事务连续更新后的状态。
图中的黄色区域就是undo log;图中展示了同一行数据的5个版本,当前最新的版本为V5,value=5,它是被transaction id为10的事务更新的。
V1~V4并不是物理上真是存在的,而是每次需要的时候根据当前版本和undo log计算出来的。
read view
按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。
因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。
当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。
在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。
这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的 db_trx_id 和这个一致性视图的对比结果得到的。
MVCC实现的整体流程
1.理解begin/start transcation和start transaction with consistent snapshot的区别
为什么要先理解这两个命令的区别呢?因为后面我们的演示用例会用到。
事务A | 事务B |
---|---|
start transacton | |
update keyvalue set value=2 where key=1; | |
select * from keyvalue; |
- 根据上一篇的结论,MySql默认的事务隔离级别为可重复读,autocommit=1,然后按照上图中的步骤依次执行,从结果可以看出来start transaction命令并没有真正的开启一个事务,因为事务B执行更新之后,事务A直接查到了事务B的更新,按照事务隔离级别为可重复读,开启事务A之后,事务A应该是读不到事务B的更新的。
猜想:start transaction命令并不是一个事务的起点,在执行到它之后的第一个操作InnoDB表的语句,事务才真正的启动。
按照这个猜想继续往下执行4和5,果然,这一次事务B的更新,在事务A中查不到了
那么,如果想要马上启动一个事务,可以使用start transaction with consistent snapshot命令
2.db_trx_id 和一致性视图的对比过程
事务A | 事务B | 事务C |
---|---|---|
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | ||
update keyvalue set value =value +1 where key =1; |
||
update keyvalue set value =value +1 where key =1; |
||
select value from keyvalue where key =1; |
||
select value from keyvalue where key =1; |
||
commit; |
假设在执行这三个事务之前,key=1对应的value=1,分析事物A的语句返回的结果是什么?为什么?
这里我们不妨假设:
1.事务A开始前,系统里边只有一个活跃的事务ID是99;
2.事务A,B,C的版本号分别是100,101,102,且当前系统中只有这四个事务;
3.三个事务开始前,(1,1)这行数据的db_trx_id是90
这样事务A的视图数组就是[99,100],事务B的视图数组是[99,100,101],事务C的视图数组是[99,100,101,102]
从图中可以看到,第一个有效更新的事务C,把数据从(1,1)改成(1,2)。这时候,这个数据的最新版本的db_trx_id是102,而90这个版本已经成为了历史版本
第二个有效更新是事务B,把数据从(1,2)变成了(1,3)。这时候,这个数据的最新版本的db_trx_id是101,而102又成为了历史版本。
现在事务A要来读数据了,它的视图数组是[99,100]。
- 找到(1,3),判断db_trx_id=101,大于100,不可见
- 接着,找到上一个历史版本,一看db_trx_id=102,大于100,不可见
- 再往前找,终于找到了(1,1),它的db_trx_id=90,小于99,可见
这样执行下来,虽然期间这一行数据被修改过,但是事务A不论在什么时候查询,看到的这行数据的结果,都是一致的,所以我们称之为一致性读。
3.更新逻辑与当前读
细心的同学可能会有疑问:
事务 B 的 update 语句,如果按照一致性读,好像结果不对哦?
事务B的视图数组是先生成的,之后事务C才提交,不是应该看不见(1,2)吗?怎么能算出来(1,3)来?
是的,如果事务 B 在更新之前查询一次数据,这个查询返回的value的值确实是 1。
但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了。因此,事务B此时的set value=value+1 是在(1,2)的基础上进行的操作。
所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。
因此,在更新的时候,当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的 db_trx_id是101。所以,在执行事务B查询语句的时候,一看自己的版本号是101,最新数据的版本号也是101,是自己的更新,可以直接使用,所以查询得到的value的值是 3。
这里我们提到了一个概念,叫作当前读。
其实,除了update语句外,select语句如果加锁,也是当前读。所以,如果把事务A的查询语句select value
from keyvalue where key
=1;修改一下,加上lock in share mode 或 for update,也都可以读到版本号是 101 的数据,返回的value的值是3。下面这两个 select语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。
select `value` from keyvalue where `key`=1 lock in share mode;
select `value` from keyvalue where `key`=1 for update;
特别感谢:
http://gk.link/a/10w98
https://www.jianshu.com/p/8845ddca3b23