基于mysql简析MVCC机制

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中)

图片.png

当插入的是一条新数据时,记录上对应的回滚段指针为NULL

图片.png

更新记录时,原记录将被放入到undo表空间中,并通过DB_ROLL_PTR指向该记录。session2查询返回的未修改数据就是从这个UNDO中返回的。MySQL就是根据记录上的回滚段指针及事务ID判断记录是否可见,如果不可见继续按照DB_ROLL_PTR继续回溯查找。
2. 通过read view判断行记录是否可见

具体的判断流程如下:
RR隔离级别下,在每个事务开始的时候,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view)。
RC隔离级别下,在事务中的每个语句开始时,会将当前系统中的所有的活跃事务拷贝到一个列表中(read view) 。
然后按照以下逻辑判断事务的可见性


图片.png
图片.png

扩展一个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 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=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=tmax(即为tx3+1)为"否",继续判断tx3在当前readview为"否",所以直接输出。
再来谈RR级别各个时间线时内部发生的情况:
T1:会话1发起一个新事物
T2:会话2发起一个新事物
T3:会话1发起 select t1查询t1表,生产了一个事务列表(readview),把当前活动事务tx1,tx2放入readview中。查询到该行的记录DB_TRX_ID为tx0,tx0 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=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=tmax(即为tx2+1)为"是",沿着回滚指针找到上一行,将事务ID赋值给tid(tx2),继续循环知道找到tx0那行记录返回。

MVCC解决了什么问题

  • MVCC使得数据库读不会对数据加锁,普通的select请求不会加锁,提高了数据库的并发处理能力。
  • 借助MVCC,数据库可以实现RC,RR等隔离特性,用户可以查询到当前数据的前一个或者前几个历史版本。保证了ACID中的I特性(隔离性)。

转载及参考:

何登成的技术博客
MVCC原理探究及MySQL源码实现分析
隔离级别的实现原理

你可能感兴趣的:(基于mysql简析MVCC机制)