MySQL MVCC机制!

MVCC是为了解决 读-写 之间阻塞的问题(排他锁会阻塞读操作),写操作还是需要加锁(Next-Key Lock)。

如果没有MVCC,那么修改数据的操作会加排它锁,其它的读写操作都会阻塞,这样的话效率会比较低。

MVCC通过Undo Log + Read View进行数据读取。

  • Undo Log保存了历史快照。
  • Read View规则判断当前版本的数据是否可见。

快照读与当前读

快照读:

快照读读取的是快照数据。

  • 不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读。

快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。

  • 既然是基于多版本,那么快照读可能读到的 并不一定是数据的最新版本,而 有可能是之前的历史版本。

快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

1
SELECT * FROM player WHERE ...

当前读:

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

加锁的 SELECT,或者对数据进行增删改都会进行当前读

1
2
3
4
5
SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁

Undo Log版本链

InnoDB 聚簇索引记录中包含 3 个隐藏的列:

DB_ROW_ID(隐藏的自增 ID):

  • 如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。

DB_TRX_ID(事务ID):

  • 记录最近更新这条行记录的事务 ID,大小为 6 个字节。

DB_ROLL_PTR(回滚指针) :

  • 指向该行回滚段的指针,大小为 7 个字节。
  • 该行记录上所有旧版本,在 undo 中都通过链表的形式组织。

在这里插入图片描述

每次对记录进行改动,都会记录一条undo日志,每条undo日志都有一个roll_pointer属性。

将这些undo日志都连起来,串成一个链表,这个链表称之为 版本链。

  • 版本链的 头节点 就是当前记录 最新的值。
  • 每个版本中包含生成该版本时对应的 事务id。

在这里插入图片描述

ReadView

ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。

判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题

ReadView主要包含4个内容:

creator_trx_id

  • 创建这个 Read View 的事务 ID。

trx_ids

  • 在生成ReadView时当前系统中 活跃的 读写事务的事务id列表(活跃:启动了但还没提交)。

up_limit_id

  • 活跃的 事务中 最小的事务 ID。

low_limit_id

  • 表示生成ReadView时系统中应该分配给下一个事务的id 值。

ReadView规则:

如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值 相同:

  • 意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

如果被访问版本的 trx_id 属性值 小于 ReadView 中的 up_limit_id 值:

  • 表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。

如果被访问版本的 trx_id 属性值 大于或等于 ReadView中的 low_limit_id 值:

  • 表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本 不可以 被当前事务访问。

如果被访问版本的 trx_id 属性值在 ReadView 的 up_limit_idlow_limit_id 之间。

需要判断一下trx_id属性值是不是在trx_ids 列表中。

  • 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。

  • 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

MVCC整体操作流程

首先 获取事务自己的版本号,也就是事务 ID。

获取 ReadView。

查询得到的数据,然后与 ReadView 中的事务版本号进行比较。

如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照。

最后 返回符合规则的数据。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性。

  • 依此类推,直到版本链中的最后一个版本。

如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

READ COMMITTED事务,在每次查询开始时都会生成一个独立的ReadView。

REPEATABLE READ事务,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。

MVCC幻读被彻底解决了吗

可重复读隔离级别(默认隔离级),根据不同的查询方式,分别提出了避免幻读的方案:

  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读。
  • 针对当前读(select ... for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。