MVCC概念和实现原理


即多版本并发控制,此文会根据以下几个点说明

  • 相关概念
  • 当前读
  • 快照读
  • 多版本并发控制原理

MVCC 基本概念

Multi-Version Concurrency Control 多版本并发控制。用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性,是通过在每个数据行上维护多个版本的数据实现的。

如下先介绍当前读和快照读的概念。

当前读

读取的是当前的最新版本数据,读取时要保证其他并发事务不能修改当前记录,所以会对读取的数据进行加锁。

两种方式都是当前读:

  • select — lock in share mode (共享锁)
  • select — for update/ insert/delete (排他锁)

举个例子(难得自己操作了一次〒▽〒):

Cmd 下操作 mysql 数据库的命令参考:mysql-cmd

  1. 数据准备:一个 dept 表,如下

  2. 开启两个事务,一个进行查询,一个进行更新;由于默认的 RR 隔离级别,第一个事务多次查询不会读取到第二个事务的更新提交内容。

    这样就实现了可重复读,如果使用当前读的命令,第一个事务就能读取到最新的更新后数据。

  3. 使用 lock in share mode; 命令,可以看到显示了最新数据

  4. 使用 for update 同样可以

快照读

就是正常的不加锁的 select 查询,读取到的数据是可见的版本,有可能是历史数据,且因为不加锁,不会阻塞其他操作。

读可见的版本,比如 RR 级别下的,同一个事务内的多次查询不会读取到其他事务更新提交的数据。

不同隔离级别下的快照读

RC 和 RR 隔离级别下的生成快照读的方式不同,与隔离级别的特性有关。

  1. RC 读已提交:每次 select 查询都会生成一个快照读,所以就可以在同一个事务内读取到其他事务更新的最新数据,但是会造成不可重复读。
  2. RR 可重复读:只有第一次 select 查询时会生成一个快照读,以后是 select 都会读取这个快照读,所以结果都一样,保证可重复读。

MVCC 实现原理

主要实现需要依靠三个隐藏字段undo logreadview

三个隐藏字段

下面是一张简单的只有三列的数据库表;

id age name DB_TRX_ID DB_ROLL_PTR DB_ROW_ID
1 18 wzc 0 0 0
2 19 abc 0 0 0

后面的三列标题即为隐藏字段

  1. DB_TRX_ID:最近修改事务 ID,表示插入或修改这条数据的最后一个事务的 ID。
  2. DB_ROLL_PTR:回滚指针,执行这条记录的上一个版本,配合 undo log 日志完成回滚。
  3. DB_ROW_ID: 实际上就是在表中没有主键时且没有唯一非空索引时会生成的隐藏字段,可能用于生成聚集索引。

    在聚集索引那篇文章我们已经见过了。

Undo log 日志

在执行 insert、update、delete 语句时会记录旧数据的日志,便于数据回滚的日志。

关于 undo log 的存储机制和工作原理,这篇文章讲的很好:MySQL回滚日志(undo log)总结

undo log 版本链

不同事务或者相同事务的对同一记录的修改,会导致该记录的 undo log 生成一个版本链。

(链表头部是最新的旧纪录,尾部是最早的旧纪录)

假设现在有一个场景:

  1. 当一个事务插入一条数据时,在数据库中就会存储一条行数据,其隐藏字段 TRX_ID 初始化为 1,回滚指针 ROLL_ID 为 NULL(未指向任何上一版本)
  2. 当其他事务更新这条数据前,数据库会先把这条数据存放到 undo log 中,且将修改后的数据的事务 ID 修改为当前正在操作的事务 ID, ROLL_ID 指向 undo log 中这条数据的存储地址用于后续回滚操作。
  3. 随着更新操作增多,就形成了版本链。

Readview

读视图,是快照读查询 SQL 语句执行时读取哪个版本数据的依据,记录和维护当前未提及的活跃事务 ID。

就靠这个实现每个事务只能读取到当前事务所处隔离级别内可以读取到的数据。

Readview 规则


其中的四个字段为:
(1)trx_id: 当前数据的事务 ID(就是最近更新该数据的事务 id)
(2)creator_trx_id 创建当前 readview 的事务 ID
(3)min_trx_id: 当前活跃事务集合 trx_ids 中的最小事务 id
(4)max_trx_id:是 readview 建立时预先生成的 id,值为集合中的最大 id + 1 。

实例一:RC 隔离级别下的 readview

在 RC 隔离级别下,每次查询操作都会创建一个 readview ,数据库会根据该 readview 的字段值与待查询数据的 undo log 版本链中的数据进行比较,以上述规则进行判断当前事务可以读取哪条数据。

如图,最终发现事务 5 的第一次查询操作只能读取 undo log 中事务 id 为 2 的数据。

也可以看到和左上角时序图的效果相同,事务 2 提交但是事务 3 未提交,所以只能读取到事务 2 修改的数据。

实例二:RR 隔离级别下的 readview

同一个事务中的 readview 只会在第一次查询时创建,后续查询复用该 readview。


文章作者: KTpro
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 KTpro !
  目录