跳转到内容

MVCC 与隔离级别

用途:解释“为什么读到了旧数据”“快照读和当前读有什么区别”

级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED(RC) 否 可能 可能
REPEATABLE READ(RR,InnoDB 默认) 否 否 快照读下基本避免(当前读仍可能,见下)
SERIALIZABLE 否 否 否(读也加锁,吞吐最低)

三件套:

  1. 隐藏列:每行带 DB_TRX_ID(最后修改它的事务 ID)与 DB_ROLL_PTR(指向 undo 中的上一版本)。
  2. undo 版本链:修改不覆盖历史,而是把旧版本留在 undo log 形成链。
  3. ReadView:事务在快照读时生成可见性视图,记录活跃事务 ID 集合,按 trx_id 沿版本链找到第一个对自己可见的版本。

RC 与 RR 的差别只在 ReadView 生成时机:RC 每条语句新建(因此能看到别人新提交的数据),RR 在事务首次快照读时创建并复用(因此整个事务内看到同一快照)。

类型 语句 行为
快照读 普通 SELECT 读版本链中可见的旧版本,不加锁
当前读 SELECT ... FOR UPDATE / LOCK IN SHARE MODE、UPDATE、DELETE、INSERT 读最新已提交版本并加锁(间隙/next-key)

同一事务里混用两者会出现“自己刚更新的数据能读到、别人刚提交的读不到”的观感,这不是 bug,是两种读语义的差异。

快照读靠 MVCC 避免幻读;当前读靠 next-key lock(记录锁 + 间隙锁) 阻止区间内插入。但仍有边界场景:事务先快照读,再执行当前读或更新,可能“看见”新行。要绝对避免需 SERIALIZABLE 或显式 FOR UPDATE。

  1. 互联网在线系统常用 RC(配合 binlog ROW 格式):间隙锁更少、锁冲突与死锁概率显著下降;RR 的“一致性视图”收益在很多业务里并不需要。
  2. 需要“读自己刚写的数据”时,用同一事务内的快照读天然满足;跨服务则考虑读写分离的路由策略(写后短时间内读主)。
  3. 长事务会让 undo 链变长,读取代价上升;监控 information_schema.innodb_trx 中的长事务。
  4. 并发扣减不要用“读-判断-写”,用条件更新(UPDATE ... SET x=x-1 WHERE x>=1)或行锁 + 幂等键。
误区 修正
RR 完全不会幻读 当前读场景仍可能,需锁或串行化
MVCC 让读一定无锁 只对快照读成立
RC 不安全 配合 ROW 格式 binlog 与正确 SQL,多数业务更合适
加隔离级别解决重复插入 用唯一约束与条件更新
认为事务开始就固定快照 RR 快照在首次一致性读时建立
  • MySQL 官方文档:InnoDB Multi-Versioning、Transaction Isolation Levels
  • 丁奇《MySQL 实战 45 讲》隔离级别与锁章节
  • 《高性能 MySQL》第 7 章

可见性判定规则(可复算)

「可见性判定规则(可复算)」小节链接

ReadView 四要素:m_ids(生成时活跃事务集合)、min_trx_id、max_trx_id、creator_trx_id。对版本链上的 trx_id:

条件 结论
等于 creator_trx_id 可见(自己改的)
小于 min_trx_id 可见(视图前已提交)
大于等于 max_trx_id 不可见(视图后开启的事务)
落在区间内且在 m_ids 中 不可见(当时未提交)
落在区间内但不在 m_ids 中 可见(当时已提交)

沿 undo 链回溯到第一个可见版本。这解释了 RC 每语句新建视图(看到最新提交)与 RR 复用视图(同一快照)的差异。

因素 影响
长事务 版本链变长,读成本上升;undo 无法回收
高频更新同一行 版本链长度与更新量成正比
purge 滞后 history list length 持续走高,读放大
大事务回滚 回滚耗时可能远超执行耗时

监控入口:information_schema.innodb_trx(长事务)、SHOW ENGINE INNODB STATUS 的 history list length、innodb_purge_threads 配置。

可复算示例:同事务内两种读结果不同

「可复算示例:同事务内两种读结果不同」小节链接
A(RR): BEGIN; SELECT balance FROM acc WHERE id=1; -- 100(快照读)
B: UPDATE acc SET balance=90 WHERE id=1; COMMIT; -- 已提交
A: SELECT balance FROM acc WHERE id=1; -- 仍 100
A: SELECT balance FROM acc WHERE id=1 FOR UPDATE; -- 90(当前读)

结论:这是设计语义而非 bug。需要“看到最新”用当前读或缩短事务;需要“整用例一致”用快照读并接受延迟。

  • 是否有快照读被误用于“必须读最新”的场景?
  • 长事务是否有阈值告警与处置策略?
  • 高频更新表是否观察过 history list length 与 purge 延迟?
  • 读写分离下“写后读不到”是否有读主兜底?
  • 并发扣减是否使用条件更新(WHERE x>=n)?
  • 从 RR 降到 RC 前是否确认 binlog 为 ROW 格式?