Time Bomb Veela
  1. 1 Time Bomb Veela
  2. 2 Quiet Storm Lyn
  3. 3 Hypocrite Nush
  4. 4 Flower Of Life 发热巫女
  5. 5 Libertus Chen-U
  6. 6 One Last You Jen Bird
  7. 7 Warcry mpi
  8. 8 Life Will Change Lyn
  9. 9 The Night We Stood Lyn
  10. 10 Last Surprise Lyn
  11. 11 かかってこいよ NakamuraEmi
2019-11-09 15:32:19

探究InnoDB的MVCC实现

锁的本质是通过串行化事务来解决并发产生的竞争问题,但串行就意味着性能的牺牲,在大多数情况下,读请求远多于写请求,效率要求相比准确率的权重占比更大,所以需要一种针对读场景下,能牺牲部分准确率换来更高效率的并发控制方案,就是MVCC了。

定义

首先先明确一下MVCC在解决问题上的定位。我们知道并发读会产生多种问题——脏读,不可重复读,幻读等等。为了解决并发问题,最彻底的方案是锁,锁有各种各样的实现上文已经介绍过了,但锁的效率问题不能忽视,不是所有情况都需要这样一种用力过猛的方案,所以衍生出了事务的4种隔离级别,最严格的串行化就是锁实现。这也就意味着,其他的隔离级别是区别于锁的实现了,其中"读已提交"和"可重复读"也就是由多版本并发控制(MVCC)实现,达到了读取过程可以完全不加锁的状态。

顾名思义,通过管理数据的多个版本来实现并发控制,让并发的事务获取到各自对应的隔离数据版本快照,操作的数据都不是狭义上的同一份了,自然就解决了竞争带来的问题。具体到结果上,事务对数据的任何修改后的commit都不会直接覆盖数据,而是产生一个新版本,那么实现两个隔离级别就很容易了。

  • 读已提交:同一事务中读取时,总是读这份数据最近被commit的版本
  • 可重复读:同一事务中读取时,总是读该事务开始前,这份数据最后一次被commit的版本

实现

在进入具体的流程分析之前,有必要先学习几个前置概念。

一致性读视图

一致性读视图(consistent read view),简称read-view。使用MVCC读取时,因为有多个版本的数据存在,很可能读的并不是最新数据,所以称作"快照读"。read-view会在事务创建后根据需要即时生成,快照读会依赖read-view去查询到自己需要的数据版本。在MySQL的默认隔离级别下,普通的select都是快照读。如果要读取最新数据,自然就要用到非MVCC的实现,也就是锁相关的读取以及更新操作了,这些操作相关的数据读取都称作"当前读"。

每份read-view都保存了一些MVCC实现相关的重要属性:

  • trx_ids: 系统内正在进行中的事务ID集合
  • low_limit_id:进行事务中最大的事务ID
  • up_limit_id:进行事务中最小的事务ID
  • creator_trx_id:创建这份视图的事务ID

事务ID

每开启一个事务,都会获得一个事务版本号作为ID识别,这个事务版本号是严格自增的,可以用来判断事务的时间顺序,同时作为事务和伴随着事务创建的read-view视图关联的依据。

行记录的隐藏列

read-view中并不存储真实数据行记录,行记录都保存在InnoDB叶子节点的数据页中。这些行记录除了常规数据记录外,还有一些特征数据保存在隐藏列中。细分为如下三种:

  • db_row_id:隐藏的行ID,用来生成默认的索引,这里不是重点就不细说了,索引在后文中详谈。
  • db_trx_id:最后一个对这该数据更新的事务ID,由此隐藏列就将事务和真实数据联系起来了。
  • db_roll_ptr:回滚指针,数据当然需要支持回滚,就靠该指针实现,指向这个记录的Undo Log信息。

Undo Log

行记录更新记录快照都保存在这里,通过db_roll_ptr以链表的形式串联起来。对于已保存的行记录快照,当系统里没有比其trx_id对应更早的read-view存在时才会被删除。

这里需要重点注意,不要把行记录更新快照和快照读这个概念搞混淆了。行记录快照都是事务操作后产生的具有真实物理结构的历史快照,而快照读是依赖read-view的查询类型,read-view是用来定义事务查询时,需要从哪份行记录快照中读的一种逻辑结构,并没有真实数据。

反过来想一想,要是每次事务启动都产生真实数据的read-view,如果一个库上百G,每次查询就有百G的数据拷贝,效率不可能及格。

流程分析

了解了以上概念,就可以仔细分析一下在查询流程中,MVCC这套机制是如何工作的了。当隔离级别为可重复读时:

  1. 开启事务,获取事务的版本号trx_id,并同时创建属于当前事务的read-view。
  2. 查询当前最新版本行记录快照,并获得隐藏列的trx_id信息。
  3. 将自己read-view的up_limit_id,low_limit_id和隐藏列的trx_id进行比对,如果trx_id小于up_limit_id,说明更新行记录的事务在此事务创建前已完成提交,判断该行对当前事务可见。如果trx_id大于low_limit_id,说明更新行记录的事务在此事务创建后才创建,那么该行对当前事务就不可见。如果up_limit_id<trx_id<low_limit_id,说明此事务创建时,更新行记录的事务可能还未结束,那就需要使用trx_ids这个进行中事务数组了。如果隐藏列的trx_id存在于该数组,说明更新行记录的事务还未结束,该行对当前事务不可见,反之可见。
  4. 经过第4步的可见性判断后,如果可见就返回数据,查询结束。如果不可见,就需要根据Undo Log继续往上查询旧数据版本并重复2,3步骤,直到查到可见数据为止。

可以看到,因为在同一事务内,read-view都是同一份,在该事务创建后对数据做更新的事务都不会保存在该read-view的trx_ids中,也就实现了"可重复读"这样的隔离级别。

当隔离级别为"读已提交"时,用这套机制实现也很简单,保证每次查询时再创建一份新的read-view即可。

小结

简单总结一下,多版本并发控制(MVCC)的"多版本"由Undo Log实现,里面存储着以事务更新为step的多版本行记录数据,是真实存在的物理结构。"并发控制"则由read-view这样一种保存着事务和行记录快照关系的逻辑结构实现。根据read-view生成策略不同,也就实现了两种不同级别的事务隔离机制。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「数据库」 标签。