MySql--MVCC

摘要

常见面试题

MVCC多版本并发控制机制

  • Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。读未提交直接读最新记录(会脏读),串行化读写都加锁,用不上 MVCC。隔离级别总表见 MySql--事务

  • 保证了事务的隔离性:读不加锁、写不阻塞读,靠「同一行多个历史版本」换并发。

  • MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。undo 日志本身见 MySql--Redo Log 和 Undo Log,行上的隐藏列见 MySql--InnoDB记录存储结构和索引页结构

快照读和当前读

MVCC 只作用在快照读上。可重复读要能同时防不可重复读和(大部分)幻读,必须把两种读分开看:

  • 快照读:普通 SELECT。不加锁,按 Read View 从 undo 版本链里挑一个可见版本。RR 下整个事务共用一个 Read View,所以两次 SELECT 看到的快照一样。

  • 当前读SELECT ... FOR UPDATE / LOCK IN SHARE MODEUPDATEDELETEINSERT。读的是已提交的最新版本,并且加锁。RR 下当前读会加临键锁(行锁 + 间隙锁),挡住别的事务往间隙里插入,用来堵当前读路径上的幻读。锁的细节见 MySql--锁

所以「可重复读是怎么实现的」不能只答 MVCC:快照读靠 MVCC,当前读靠临键锁。两者叠在一起,才是 InnoDB 默认 RR 的完整实现。

undo日志版本链

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。对应行记录里的 DB_TRX_ID(6 字节)和 DB_ROLL_PTR(7 字节);没主键时还有 DB_ROW_ID

read-view

在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句, 事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
补充:Read View 也不是 BEGIN 时生成的,而是本事务里第一次快照读时生成的。只跑 UPDATE/DELETE 不会走这套快照。InnoDB 源码里 max_id 对应 low_limit_id(下一个即将分配的事务 id),min_id 对应 up_limit_id

版本链比对规则

    1. 如果查询结果中row的 trx_id<min_id,表示这个版本是已提交的事务生成的,这个数据是可见的
    1. 如果查询结果中row的 trx_id>max_id,表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的)
    1. 如果查询结果中row的 min_id <=trx_id<= max_id,那就包括两种情况

a. 若 row 的 trx_id 在read-view视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
b. 若 row 的 trx_id 不在read-view视图数组中,表示这个版本是已经提交了的事务生成的,可见。

    1. 对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的 trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。

MySql可重复读隔离级别是如何实现的,谈谈你对MVCC的理解?

  • MVCC 是什么:Multi-Version Concurrency Control,多版本并发控制。InnoDB 给每一行保留多个历史版本(undo 链),读请求按自己的 Read View 挑一个可见版本,写请求去改最新版本。读不必等写、写不必挡快照读,这是它和「读写锁互斥」最大的差别。只在 READ COMMITTEDREPEATABLE READ 下启用。

  • 三个零件:

    • 行上的 DB_TRX_ID + DB_ROLL_PTR,把每次修改串成 undo 版本链。
    • Read View:生成那一刻的活跃事务名单(m_ids)、最小活跃 id(min_id/up_limit_id)、下一个待分配 id(max_id/low_limit_id)。
    • 比对规则:从最新版本沿 roll_pointer 往回走,按上文 1~4 条判断可见性;自己改的永远可见,生成视图之后才启动的事务不可见。
  • 可重复读怎么落地(默认隔离级别):

    • 快照读(普通 SELECT):整个事务只用第一次 SELECT 时生成的那一个 Read View,后面再 SELECT 仍按这张视图走版本链。别的事务提交的修改、删除,你看不见,所以不会不可重复读;它提交的新插入你也看不见,所以普通 SELECT 路径上的幻读也被挡住了。
    • 当前读SELECT ... FOR UPDATE、UPDATE、DELETE):必须读最新已提交数据,MVCC 帮不上忙。RR 给扫描到的记录加行锁,给记录之间的间隙加间隙锁,合称临键锁,禁止别的事务往这个范围里插入,从而堵住当前读路径上的幻读。
    • 对照 RC:RC 每次快照读都新建 Read View,所以能看见别的事务已提交的改动,会出现不可重复读和幻读;RC 默认不加间隙锁,并发更好、幻读风险更大。
  • 对 MVCC 的理解,面试说到这就够用:

    • 它用空间(undo)换并发,不是用锁把读挡住。
    • 它解决的是的隔离,不负责写冲突:两个事务改同一行,照样靠行锁互斥,后提交的等先提交的,不会靠版本号自动合并。
    • 它不能替代当前读。SELECT 可重复,紧接着 UPDATE 却可能作用于更新后的行(半可见),这是快照读和当前读混用时的经典坑。
    • SQL 标准把 RR 标成「可能幻读」,InnoDB 用 MVCC + 临键锁把日常场景堵住了,但不是可串行化,极端交叉场景仍可能出现类似幻读的现象。

一句话:InnoDB 的可重复读 = 一个事务一个 Read View 的 MVCC 快照读 + 当前读上的临键锁。MVCC 让普通 SELECT 可重复且尽量不阻塞,锁负责在真正改数据时把间隙也占住。