MVCC
MVCC(Multi-Version Concurrency Control),直翻过来就是多版本并发控制。对MVCC对应的就是加锁的并发控制LBCC(Lock-Based Concurrency Control)。MVCC最大的好处就是读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。
MVCC是multiversion concurrency control的简称,也就是多版本并发控制,是个很基本的概念。MVCC的作用是让事务在并行发生时,在一定隔离级别前提下,可以保证在某个事务中能实现一致性读,也就是该事务启动时根据某个条件读取到的数据,直到事务结束时,再次执行相同条件,还是读到同一份数据,不会发生变化(不会看到被其他并行事务修改的数据)。MVCC是需要配合事务隔离级别的。
有了 MVCC 就可以提高事务的并行度,因为可以利用锁机制实现资源控制而无需等待其他事务先执行。
在了解MVCC之前需要理解一下几个概念:
基础概念
在数据库并发读写的时候,多会话的读写可能会产生不一致的现象,MVCC就是为了避免这种情况,对数据库进行并发访问控制。避免脏读等现象最简单的一种方式就是加锁,达到读写串行,就不会产生脏读等现象,但是串行读写并发读写性能无法达到生产数据库环境要求。
MVCC实现原理
上述现象在数据库中大家经常看到,但是数据库到底是怎么实现的,深究的人就不多了。
其实原理很简单,InnoDB数据库就是通过UNDO和MVCC来实现的。
1. 旧数据存储在UNDO中,再通过DB_ROLL_PTR 回溯查找历史版本
首先InnoDB每一行数据还有一个DB_ROLL_PTR(回滚指针),用于指向该行修改前的上一个历史版本(InnoDB里,会将 row data修改前的旧数据存储在UNDO中)
当插入的是一条新数据时,记录上对应的回滚段指针为NULL
更新记录时,原记录将被放入到undo表空间中,并通过DB_ROLL_PTR指向该记录。session2查询返回的未修改数据就是从这个UNDO中返回的。MySQL就是根据记录上的回滚段指针及事务ID判断记录是否可见,如果不可见继续按照DB_ROLL_PTR继续回溯查找。
2. 通过read view判断行记录是否可见
具体的判断流程如下:
RR隔离级别下,在每个事务开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)。
RC隔离级别下,在事务中的每个语句开始时,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view) 。
然后按照以下逻辑判断事务的可见性
扩展一个Read View的数据结构记录版本数据,它有三个部分:
(1) 当前活跃的事务列表 ,即[101,102]
(2) Tmin ,就是活跃事务的最小值, 在这里 Tmin = 101
(3) Tmax, 是系统中最大事务ID(不管事务是否提交)加上1。 在这里例子中,Tmax = 103
现在来模拟一下这个快照读这个场景
首先假设t1表中的当前DB_TRX_ID为tx0
时间点 | 会话1 | 会话2 | 会话3 |
---|---|---|---|
T1 | start tx1 | ||
T2 | start tx2 | ||
T3 | select t1 | ||
T4 | start tx3 | ||
T5 | update t1;commit | ||
T6 | select t1 | ||
T7 | update t1;commit | ||
T8 | select t1 |
先来谈RC级别各个时间线时内部发生的情况:
T1:会话1发起一个新事务
T2:会话2发起一个新事物
T3:会话1发起了select t1查询t1表,生成了一个事务列表(readview),把当前活动事务tx1,tx2放入readview中。查询到的该行记录DB_TRX_ID为tx0,tx0<tmin(即为tx1),直接将该行输出。
T4:会话3发起一个新事物
T5:会话2发起了update t1更新t1表并提交,把该行旧的记录复制到undo log中,该行记录的DB_TRX_ID更新为tx2,该行的DB_ROLL_PTR指向undo log中DB_TRX_ID为tx0旧的记录的位置,记录redo log。
T6:会话1发起select t1查询t1表,生成了一个事务列表(readview)把当前的活动事务tx1,tx3放入readview中。并查询到该行记录DB_TRX_ID为tx2,由于tx2<tmin(即为tx1)为"否"所以继续判断,tx2>=tmax(即为tx3+1)为"否",继续判断tx2在当前readview中为"否",所以直接将该行输出。
T7:会话3发起了update t1更新t1表并提交,把该行的记录复制到undo log中,该行记录的DB_TRX_ID更新为tx3,该行的DB_ROLL_PTR指向undo log中DB_TRX_ID为tx2旧记录位置,tx2旧的记录回滚指针继续指向tx0旧记录位置,记录redo。
T8:会话1发起select t1查询t1表,生成了一个事务列表(readview)把当前的互动事务tx1放入readview中。并查询到该行记录的DB_TRX_ID为tx3,由于tx3<tmin(即为tx1)为"否"所以继续判断,tx3>=tmax(即为tx3+1)为"否",继续判断tx3在当前readview为"否",所以直接输出。
再来谈RR级别各个时间线时内部发生的情况:
T1:会话1发起一个新事物
T2:会话2发起一个新事物
T3:会话1发起 select t1查询t1表,生产了一个事务列表(readview),把当前活动事务tx1,tx2放入readview中。查询到该行的记录DB_TRX_ID为tx0,tx0<tmin(即为tx1),直接将该行输出。
T4:会话3发起一个新事物
T5:会话2发起了update t1更新t1表并提交,把该行旧的记录复制到undo log中,该行记录的DB_TRX_ID更新为tx2,该行的DB_ROLL_PTR指向undo log中DB_TRX_ID为tx0的旧的记录位置,记录redo log。
T6:会话1发起select t1查询t1表,由于是RR隔离级别,所以利用第一次查询生成的readview,查询到该行记录DB_TRX_ID为tx2,由于tx2<tmin(即为tx1)为"否"所以继续判断,tx2>=tmax(即为tx3+1)为"否",继续判断tx2在readview中,结果为"是",所以沿着回滚指针找到上一行,将事务ID赋值给tid(tx0)。
T7:会话3发起update t1查询t1表并提交,把该行旧的记录复制到undo log中,该行记录的DB_TRX_ID更新为tx3,该行的DB_ROLL_PTR指向undo log中DB_TRX_ID为tx2的旧的记录位置,记录redo log。
T8:会话1发起select t1查询t1表,由于是RR隔离级别,所以利用第一次查询生成的readview,查询到该行记录DB_TRX_ID为tx3,由于tx3<tmin(即为tx1)为"否"所以继续判断,tx3>=tmax(即为tx2+1)为"是",沿着回滚指针找到上一行,将事务ID赋值给tid(tx2),继续循环知道找到tx0那行记录返回。
MVCC解决了什么问题
- MVCC使得数据库读不会对数据加锁,普通的select请求不会加锁,提高了数据库的并发处理能力。
- 借助MVCC,数据库可以实现RC,RR等隔离特性,用户可以查询到当前数据的前一个或者前几个历史版本。保证了ACID中的I特性(隔离性)。