MySQL事务(二)事务隔离的实现原理:一致性读


今天我们来学习一下MySQL的事务隔离是如何实现的。如果你对事务以及事务隔离级别还不太了解的话,这里。

好的,下面正式进入主题。事务隔离级别有4种:读未提交、读提交、可重复读和串行化。首先我们来说一下读未提交和串行化。

  • 读未提交:性能最好,因为不加锁,所以可以理解为没有隔离。
  • 串行化:读加共享锁,其他事务可并发读,但不能写;写加排他锁,其他事务不能并发写也不能并发读。

这两种方式要么啥都不管,并发性能最好,但也最多问题;要么管得很严,无法并发处理,实现简单。

另外两种,读提交和可重复读,的实现方式就有考究了。

可重复读

首先我们来看一下可重复读是如何实现的。

在可重复读隔离级别下,事务在启动的时候就“拍了个快照”,并且这个快照是基于整个库的。而“快照”在计算机里是拷贝了一份当前的副本文件,但在数据库并发访问场景下,不可能真的拷贝一份数据副本。

实际上,这个快照是基于InnoDB在实现MVCC时用到的一致性读视图来实现的。

MVCC的全称是“多版本并发控制”。这项技术使得InnoDB的事务隔离级别下执行一致性读操作有了保证,换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值。这是一个可以用来增强并发性的强大的技术,因为这样的一来的话查询就不用等待另一个事务释放锁。这项技术在数据库领域并不是普遍使用的。一些其它的数据库产品,以及MySQL其它的存储引擎并不支持它。

如何实现“快照”

InnoDB里面每个事务有一个唯一的事务ID,叫作transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。

数据表中的一行记录,其实可能有多个版本(row),每个版本都有自己的row trx_id。如图1所示:


图1 行状态变更图

图中虚线框里是同一行数据的4个版本,当前最新版本是V4,k的值是22,它是被transaction id为25的事务更新的,因此它的row trx_id是25。

其实除了最新版本V4外,其他三个版本实际上是不存在的,它们是由undo log和最新版本数据计算得到的。其中undo段就是图中的虚线箭头的U1、U2、U3,例如V1版本就是根据V4依次执行U3、U2、U1计算得到的。

undo log是回滚日志,保存的是逻辑格式的日志,可用于事务回滚,也可以用于MVCC。

按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但之后在这个事务执行期间,其他事务的更新对它不可见。

InnoDB为每个事务构成一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务ID。“活跃”指启动了但还没有提交。

数组里事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1记为高水位。这个视图数组加高水位就组成了当前事务的一致性视图(read-view)。

而数据版本的可见性规则,就是基于数据的row trx_id和这个一致性视图的对比结果得到的。


图2 数据可见性规则

对于当前事务的启动瞬间来说,一个数据版本的row trx_id有以下几种可能:

  • 如果落在绿色部分,表示这个版本是已经提交的事务或者是当前事务自己生成的,这个数据是可见的。
  • 如果落在红色部分,表示这个版本是由将来启动的事务生成的,不可见。
  • 如果落在橙色部分,就有两种情况:
    • 若row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见。
    • 若row trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。

比如图1中的数据来说,如果有一个事务,它的低水位是18,那么当它访问这一行数据时,就会从V4通过U3计算出V3,所以在它看来,这一行的值是11。

在更新时如何使用一致性读


图3 示例1

我们来看示例1,如果事务B在事务C更新之前查询,这个查询返回值是1。但是当它要去更新数据时,就不能在历史版本上更新了,否则事务事务C的更新就会丢失。

这里就用到一条规则:更新数据都是先读后写,而这个读只能是读当前的值,称为“当前读”(current read)。

因此事务B更新时,当前读拿到的数据是(1, 2),更新后是(1, 3),并且row trx_id是101。

事务B后续查询时,看到最新数据的版本号是101,而自己也是101,就直接返回,得到的k值是3。


图4 示例2

我们再来看示例2,示例2与示例1的区别在于,事务C'在事务B的写读操作后提交。

事务C'在提交前对行加写锁。而事务B是当前读,而且必须要加锁,因此被锁住了,必须等到事务C'释放这个锁,才能继续它的当前读。

到这里,把一致性读、当前读和行锁串起来了。

小结

本节问题,事务的可重复读隔离级别是如何实现的?

可重复读的核心就是一致性读;而事务更新数据的时候,只能用当前读。如果当前的记得的行锁被其他事务占用的话,就需要进入锁等待。

读提交

读提交的实现方式跟可重复读类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每个语句执行前都会重新算出一个新的视图。

参考资料

  • 03 | 事务隔离:为什么你改了我还看不见?
  • 08 | 事务到底是隔离的还是不隔离的?