多版本并发控制(MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

基本思想

在封锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的,而 MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这一点和 CopyOnWrite 类似。

在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据新增一个版本快照

脏读不可重复读最根本的原因是读取到其他事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算脏读。

版本号

  • 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
  • 事务版本号 TRX_ID:事务开始时的系统版本号。

Undo 日志

MVCC 的多版本指额是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。
例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。

INSERT INTO t(id, x) VALUES(1, “a”);
UPDATE t SET x=“b” WHERE id=1;
UPDATE t SET x=“c” WHERE id=1;

因为没有使用 START TRANSACTION 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。多版本并发控制
INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。

ReadView

MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1,TRX_ID_2,…},还有该列表的最小值 TRX_ID_MIN 和最大值 TRX_ID_MAX。
多版本并发控制
在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用:

  • TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所以未提交事务之前进行更改的,因此可以使用。
  • TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。
  • TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
    (1)提交读:如果 TRX_ID 在 TRX_IDs 中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
    (2)可重复读:都不可以使用。因为如果可以使用的话,那么其他事务也可以读到这个数据行并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。

在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断

快照读与当前读

  1. 快照读
    MVCC 的 SELECT 操作都是快照中的数据,不需要进行加锁操作。

SELECT * FROM table …;

  1. 当前读
    MVCC 其它对数据库进行的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作

INSERT;
UPDATE;
DELETE;

在进行 SELECT 操作时,可以强制指定进行加锁操作。一下第一个语句需要加 S 锁,第二个需要加 X 锁

SELECT * FROM table WHERE ? lock in share mode;
SELECT * FROM table WHERE ? for update;

Next-Key Locks

数据库使用锁是为了支持更好的并发,提供数据的完整性和一致性。InnoDB 是一个支持行锁的存储引擎,锁的类型有:共享锁(S)、排他锁(X)、意向共享锁(IS)、意向排他锁(IX)。为了提供更好的并发,InnoDB 提供了非锁定读:不需要等待访问行上的锁释放,读取行的一个快照。该方法是通过 InnoDB 的一个特性:MVCC 来实现的。

InnoDB 有三种行锁的算法:

  1. Record Lock:单个行记录上的锁。
  2. Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。间隙锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
  3. Next-Key Locks:1 + 2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻影读问题,Next-Key Locks 就是为了解决这个问题存在的,在可重复读隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。

Record Lock

锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Lock 依然可以使用。

Gap Lock

锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

它是 Record Lock 和 Gap Lock 的结合,不仅锁定一个记录上的索引,也锁定索引之间的记录。它锁定一个前开后闭区间,例如

相关文章: