MVCC

事务 id

InnoDB 行格式 中介绍每条记录中都有三个隐藏列,其中一个是 trx_id,记录着修改该记录的最新事务 id

undo log 格式 中介绍每条 update undo log 和 delete undo log 中也有一个 trx_id 属性,记录着修改 undo log 对应原记录的事务 id

MySQL 服务器会在内存中维护一个全局变量,每当需要为某个事务分配事务 id 时,就会把该变量作为事务 id 分配给该事务,并且把该变量自增 1

注意:只有在事务中第一次执行增删改操作时才会触发事务 id 的分配,所以如果只有读操作不会分配事务 id,而是生成一个很大的值,可以看作是伪事务 id,详情可见 mysql中事务id,有啥用?

下面总结一些常用的命令:

版本链

undo log 中浅浅的提了一下版本链的概念,本部分详细介绍!

记录的隐藏列中有一个 roll_pointer 属性,表示回滚指针,指向记录最新的 undo log,而删除更新的 undo log 中也有 roll_pointer 指针,会形成一个链表,表示该记录的版本链

7

注意:为了统一格式,图中的 undo log 中有些属性是没有的,关于 undo log 格式的详细介绍可见 undo log 格式

通过版本链,可以找到该记录任何版本的数据,从而可以在并发事务中控制哪个版本对哪个事务可见,这种机制称为多版本并发控制 (Multi Version Concurrency Control,MVCC)

注意:只有普通 select 语句才可以读到历史版本,被称为快照读;对于select ... for update只能读到最新数据,也就是 B+ 树页面的记录,被称为当前读

延伸:对于 update 和 delete 操作,也只能修改最新数据,也就是 B+ 树页面的记录,如果该记录正在被其它事务修改且没有提交,该记录会被加锁,当前事务的操作会被阻塞

ReadView

通过 MVCC 可以控制事务可见的版本,而 MVCC 是通过 ReadView 实现,每个事务会生成一个 ReadView,不同生成时机可以实现不同的可见性效果

MySQL 中有四种隔离级别:READ UNCOMMITTED (读未提交)、READ COMMITTED (读已提交)、REPEATABLE READ (可重复读)、SERIALIZABLE (串行化),详情可见 事务隔离级别

每个 ReadView 中都包含四个字段:

每个 ReadView 都将记录中的 trx_id 划分为三种情况

8

一个事务在访问记录某个版本时,通过以下步骤来判断该版本是否对该事务可见:

9

READ COMMITTED 实现细节

在事务中每次读取数据前都生成一个 ReadView,所以一个事务中可能会生成多个 ReadView

10

对于当前事务,操作 1 和操作 2 都无法看见事务 A 的修改,因为此时事务 A 还没有提交;操作 3 和操作 4 可以看见事务 A 的修改,因为此时事务 A 已经提交

在生成的四个 ReadView 中,ReadView1 和 ReadView2 中的 m_idx 列表中有事务 A 的 id,ReadView3 和 ReadView4 中的 m_idx 列表中没有事务 A 的 id,因此此时事务 A 已提交

在 READ COMMITTED 的隔离级别中,只需要保证读取的数据是已提交的即可,也就是避免出现脏读,但依旧会出现不可重复读的现象,因为当前事务中可以读到在执行事务期间其它事务提交后修改的数据

REPEATABLE READ 实现细节

在事务中第一次读取数据前生成一个 ReadView,所以一个事务中只会生成一个 ReadView

11

对于当前事务,四个操作都无法看见事务 A 的修改,整个事务期间只有一个 ReadView,而 ReadView 生成时事务 A 并没有提交,所以事务 A 的 id 在 ReadView 中的 m_idx 列表中

在 REPEATABLE READ 的隔离级别中,由于当前事务中不可以看见在执行事务期间其它事务提交后修改的数据,所以避免出现不可重复读

上面是更新的操作,下面再来一种删除的操作,先看图:

24

在时刻 1 和时刻 4,事务 A 两次查询的结果是一致的,因为这是在 REPEATABLE READ 的隔离级别中,利用 ReadView 保证可重复读

疑问:id = 2的记录在事务 B 提交后在 purge 阶段 被加入到垃圾链表中。在时刻 4 事务 A 执行select时会全表扫描,可是被删记录已经不在聚簇索引中,那怎么找到删除记录的版本链呢?

其实事务 B 提交后不会立刻执行 purge 阶段,只有当系统中最早产生的 ReadView 都不再访问 unod log 后,才会开始执行 purge 阶段