多版本控制MVCC

904 阅读6分钟

Mutil-Version Concurrency Control (多版本控制)

  • Innodb的多版本控制可以在避免加锁的前提下实现读已提交、可重复读的隔离效果。
    • 优点:读的时候不用加锁,可以增加并发读的性能。修改数据的时候还是会加锁,避免同一个数据被同时修改。
    • 缺点:需要存储更多的字段,牺牲了空间。

概念解释

  • rowid:
    • rowid是用于标识每一行数据的唯一的标识符。
    • 很多网站说mysql的rowid是主键来代替,但MVCC机制中,明显单靠主键是无法确定唯一行的,具体实现暂不清楚。
  • 版本号:
    • 版本号代表执行操作的事务id,用于记录数据被哪个事务修改。事务id是数据库维护的一个自增id。
    • 后面用到的版本号有插入版本号、删除版本号,即用来记录执行相关操作的事务id。一般直接把插入版本号称为记录的版本号。
  • ReadView:
    • 数据库内部保存的活跃事务id列表。活跃事务id即开始了但还未提交的事务。假设事务61开启了事务后,事务列表可能处于如下情况:tx61 -> tx60 -> tx56 -> tx50.
    • **ReadView是当前事务向数据库获取的活跃事务列表。**假设事务61开启后向系统获取ReadView,则ReadView为:[tx60,tx56,tx50].
  • 高水位:活跃事务列表的最大事务id。
  • 低水位:活跃事务列表的最小事务id。

MVCC内部怎么实现

  • 定义上:

    • 在数据库每一行上增加了三个隐藏字段:插入版本号,删除版本号,回滚指针。
      • 插入版本号:记录插入这条记录的事务id。这里“插入”也可能是更新,MVCC不会修改原数据,更新数据是把原数据的删除版本号记下当前的事务id,再已当前事务id的名义插入新的记录。
      • 删除版本号可能为空,若这条记录已失无效(失效包括被删除或者数据已被更新),则记录失效的版本号。
      • 回滚指针:指向上一版本的rowid。rowid可以理解为唯一表示每一行的标识。
  • 操作:增删改

    • 插入:假如事务A的id是100,插入了一条记录,提交。
      • 插入记录.jpg
        • 说明:灰色字代表隐藏列,黑色则是业务创建的列
    • 更新:假如事务B的id是200,把id=1的记录的name换成“小明2”,并提交。
      • 更新记录.jpg
    • 删除:假如事务C的id是300,把id=1的记录删除,并提交。
      • 删除记录.jpg

MVCC如何实现读已提交、可重复读?

读已提交、可重复读是啥?

  • 读已提交:其他事务提交了,当前事务就能立刻读取到。
    • 读已提交.jpg
  • 可重复读:事务对同一个值的两次读取都必须一样,即使中间该值被其他事务修改并提交也不会影响。
    • 可重复读.jpg

MVCC实现“读已提交”级别:

  • 事务A与事务B的行为:
    • 读已提交.jpg
  • 过程:
    1. 事务A查询id=1的x值:
      • 初始化记录
      • 事务A先从数据库系统导入最新的ReadView,此时ReadView:[],即除了事务A没有活跃事务。此时高水位和低水位都是100.事务A查询查询到最新的记录,x=100,版本号86,此时版本号小于低水位100,表示该记录有效,直接输出。
        • 补充1:如果版本大于高水位,表示该记录是当前事务获取了ReadView之后才插进去的,对当前事务不可读,这里应当读取通过回滚指针回滚到可读版本的记录。
        • 补充2:如果当前记录版本大于等于低水位,又小于等于高水位,即版本号属于区间[低水位,高水位],则分以下情况:
          1. 记录版本号在ReadView之中:表示操作该记录的事务仍未提交,不能读取该记录数据,要通过回滚指针往前回滚记录。
          2. 记录版本号不在ReadView之中:表示操作该记录的事务已提交,该记录可被读取。
          3. 记录版本号等于当前版本号(当前版本号一定位于系统活跃事务列表中,因此事务id一定在低水位和高水位之间),则该记录与当前事务相关,因此该记录对当前记录可见,可以输出。
            • 第3小点看似是第2小点的特殊情况,其实不太一样。表面上看,当前版本号也不在ReadView中,所以满足第2小点的要求,结果是可以输出,满足第2小点的结果。但当前事务跟其他不在ReadView中出现的事务不一样,其他的事务是已经提交了,但当前事务并还没有提交,所以性质不太一样。
    2. 事务B把id=1的x修改为200,并提交事务。
      • 修改记录
    3. 事务A再次查询id=1的x值:
      • 事务A再次从数据库系统导入最新的ReadView,此时ReadView:[],低水位100,高水位200。读取表,发现x=200,记录版本号200。发现200属于区间[100,200],且该版本号200不在ReadView中,则该结果x=200可以输出。
        • 补充1:若事务B没有提交呢?则读到的ReadView:[200],那么记录x=200的版本号会出现在ReadView中,往前回滚到rowid = 2,即x=100,版本号为86,重新判断版本号86是否可以输出(第一小点已经说明过86为什么是可以输出的),若可以,则输出,不行就再一次回滚。
  • 总结:读已提交级每一步操作都会从新读取ReadView,刷新本地事务缓存的活跃事务列表,从而确保可以读取到新提交的记录时无需回滚版本。

MVCC实现“可重复读”级别:

  • 事务A与事务B的行为录:
    • 可重复读.jpg
  • 过程:
    1. 事务A开启:事务A先从数据库系统导入最新的ReadView,此时ReadView:[],即除了事务A没有活跃事务。此时高水位和低水位都是100。
    2. 事务A查询id=1的x值:
      • 初始化记录
      • 事务A查询查询到最新的记录,x=100,版本号86,此时版本号小于低水位100,表示该记录有效,直接输出。
    3. 事务B把id=1的x修改为200,并提交事务。
      • 修改记录
    4. 事务A再次查询id=1的x值:
      • 事务A会沿用一开始就导入的ReadView(第一小点提及),发现200超出了高水位100,往前回滚到rowid=2的记录。此时x=100,版本号是86,小于低水位100,该记录有效,可以输出。
  • 总结:在可重复读级别中,每个事务只在最开始导入ReadView,后面操作延用这个ReadView,从而确保同一条记录多次读取最终追踪到的版本都是一致的。

引用