InnoDB MVCC 快照读与当前读的区别及 RC/RR 下的表现
背景
InnoDB 通过多版本并发控制(MVCC)实现读写不互相阻塞:普通的 SELECT 读取的是数据的某个历史快照,而加锁读和写操作读取的是最新已提交的数据。理解快照读与当前读的区别,以及它们在 READ COMMITTED(RC)与 REPEATABLE READ(RR)隔离级别下的表现,有助于分析幻读、不可重复读等问题。
隐藏列与 undo 版本链
InnoDB 聚簇索引记录包含隐藏列:DB_TRX_ID 记录最近一次修改该行的事务 ID,DB_ROLL_PTR 指向 undo log 中该行的上一个版本,没有显式主键时还会有 DB_ROW_ID。每次更新都会把旧版本写入 undo log,并通过 DB_ROLL_PTR 串成版本链,链头是最新版本。
Read View 可见性规则
Read View 在创建时记录:m_ids(当前活跃的读写事务 ID 列表)、min_trx_id(活跃事务中最小的 ID)、max_trx_id(下一个将分配的事务 ID)、creator_trx_id(创建者自身事务 ID)。判断某个版本的 DB_TRX_ID 是否可见:等于 creator_trx_id 则可见(自己的修改);小于 min_trx_id 则可见(生成前已提交);大于等于 max_trx_id 则不可见;介于两者之间时,若在 m_ids 中则不可见,否则可见。不可见时沿 undo 版本链继续向前查找,直到找到可见版本。
RC 与 RR 的差异
差别在于 Read View 的创建时机。RC 下每次执行快照读都会生成新的 Read View,因此能读到其他事务刚提交的数据,出现不可重复读。RR 下事务中第一次快照读时创建 Read View 并一直复用,因此同一事务内多次普通查询结果一致。示例(表 t(id PK, v),初始 id=1, v=10):
-- 会话 A
BEGIN;
SELECT v FROM t WHERE id=1; -- 返回 10
-- 会话 B
BEGIN;
UPDATE t SET v=20 WHERE id=1;
COMMIT;
-- 会话 A
SELECT v FROM t WHERE id=1; -- RC 返回 20,RR 仍返回 10
快照读 vs 当前读(SELECT … FOR UPDATE / UPDATE)
快照读指不加锁的普通 SELECT,依据 Read View 读取可见版本。当前读包括 SELECT … FOR UPDATE、SELECT … LOCK IN SHARE MODE、UPDATE、DELETE、INSERT,它们总是读取记录的最新已提交版本,并加上相应的行锁(RR 下还可能加间隙锁以防止幻读)。接上例,在 RR 下会话 A 再执行 SELECT v FROM t WHERE id=1 FOR UPDATE 将返回 20;若执行 UPDATE t SET v=v+1 WHERE id=1,则基于最新值 20 更新为 21,之后会话 A 的快照读也能看到自己修改的结果 21。这也是 RR 下同一事务中快照读与当前读结果可能不一致的原因。
小结
MVCC 依靠隐藏列、undo 版本链和 Read View 实现一致性非锁定读。RC 每次快照读都新建 Read View,RR 则在首次快照读时创建并复用;当前读始终读取最新数据并加锁。业务中如需基于最新数据做判断和修改,应使用当前读,而不是依赖快照读的结果。
- 点赞
- 收藏
- 关注作者
评论(0)