InnoDB 中的 MVCC

MVCC

多版本并发控制 (MVCC, Multi-Version Concurrency Control),一种并发控制机制,用于在多个并发事务同时读写数据库时保持数据的一致性和隔离性。通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。

查询操作

当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的,因此事务不会读取其他事务尚未提交的修改。流程如下:

  1. 对于查询,事务会查找符合条件的数据行,并选择符合其事务开始时间的数据版本进行读取。
  2. 如果某个数据行有多个版本,事务会选择不晚于其开始时间的最新版本,确保事务只读取在它开始之前已经存在的数据。
  3. 事务读取的是快照数据,因此其他并发事务对数据行的修改不会影响当前事务的读取操作。

更新操作

当一个事务执行更新操作时,它会生成一个新的数据版本,并将修改后的数据写入数据库。流程如下:

  1. 对于写操作,事务会为要修改的数据行创建一个新的版本,并将修改后的数据写入新版本。
  2. 新版本的数据会带有当前事务的版本号,以便其他事务能够正确读取相应版本的数据。
  3. 原始版本的数据仍然存在,供其他事务使用快照读取,这保证了其他事务不受当前事务的写操作影响

事务的提交与回滚

当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。

当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。

版本的回收

MVCC 定期回收版本,防止数据库中的版本无限增长,删除已经不再需要的旧版本数据,释放空间

一致性非锁定读与锁定读

一致性非锁定读

使用版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见

在 InnoDB 存储引擎中,多版本控制 (multi versioning) 就是对非锁定读的实现。如果读取的行正在执行 DELETEUPDATE 操作,这时读取操作不会去等待行上锁的释放。相反地,InnoDB 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read)

读取未提交读取已提交的隔离等级下,执行普通的 SELECT 语句,会使用一致性非锁定读,在可重复读隔离等级下防止部分幻读

锁定读

锁定读下,读取的是数据的最新版本,这种读也被称为当前读(current read)。锁定读会对读取到的记录加锁。在执行更新操作时,使用锁定读。

在一致性非锁定读下,即使读取的记录已被其它事务加上 X 锁,这时记录也是可以被读取的,即读取的快照数据。上面说了,在重复读隔离等级下 MVCC 防止了部分幻读, 是指在一致性非锁定读情况下,只能读取到第一次查询之前所插入的数据。

如果是当前读 ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以, InnoDB 在实现重复读隔离等级时,如果执行的是当前读,则会对读取的记录使用 Next-key Lock ,来防止其它事务在间隙间插入数据。

InnoDB 对 MVCC 的实现

实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_IDRead View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。

隐藏字段

InnoDB 为每行数据添加三个隐藏字段:

  • DB_TRX_ID:表示最后一次插入或更新该行的事务 id。此外,DELETE 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除
  • DB_ROLL_PTR:回滚指针,指向该行的 undo log 。如果该行未被更新,则为空
  • DB_ROW_ID:如果没有设置主键且该表没有唯一非空索引时,InnoDB 会使用该 id 来生成聚簇索引

Read View

判断可见性,保存了 “当前对本事务不可见的其他活跃事务”

m_low_limit_id:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见

m_up_limit_id:活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_id 为 m_low_limit_id。小于这个 ID 的数据版本均可见

m_ids:Read View 创建时其他未提交的活跃事务 ID 列表。创建 Read View 时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)

m_creator_trx_id:创建该 Read View 的事务 ID

undo log

  • 事务回滚时用于将数据恢复
  • 在 MVCC 中,读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读

在 InnoDB 中分为两种:

  1. insert undo log:在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,故该 undo log 可以在事务提交后直接删除。
  2. update undo log:update 或 delete 操作中产生的 undo log。该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge 线程 进行最后的删除。不同事务或者相同事务的对同一记录行的修改,会使该记录行的 undo log 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。