快照读不用锁靠Undo Log版本链实现,SELECT通过ReadView沿DB_ROLL_PTR链追溯可见版本;ReadView用m_up_limit_id、m_low_limit_id和m_creator_trx_id三值判断版本可见性。

快照读为什么不用锁?靠的是Undo Log版本链
MySQL的SELECT不加锁,不是“放任不管”,而是把一致性判断逻辑从“加锁阻塞”搬到了“按需追溯旧版本”。核心在于:每行数据通过DB_ROLL_PTR连成一条单向链表,链上的每个节点都是该行在某个事务修改前的快照,物理存储在Undo Log里。
- 每次
UPDATE或DELETE都会生成新版本,并把DB_TRX_ID设为当前事务ID,DB_ROLL_PTR指向上一个Undo Log记录 - 读操作不查最新行,而是拿着自己的
ReadView,沿着版本链往前找——第一个满足可见性规则的版本就是它该看到的数据 - 这意味着:100个并发
SELECT可以同时遍历同一条版本链,互不干扰;而UPDATE只管追加新版本,完全不碰旧版本的内存或磁盘位置
ReadView怎么决定哪个版本“可见”?关键看三个ID
ReadView不是时间戳,也不是快照副本,而是一组事务ID边界值。它的判断逻辑直接决定你查到的是“刚提交的新值”还是“事务开始时的老值”。
-
m_up_limit_id(低水位):小于它的事务ID,一定已提交 → 对当前事务可见 -
m_low_limit_id(高水位):大于等于它的事务ID,一定未提交或正在运行 → 不可见 -
m_creator_trx_id:当前事务自己的ID → 自己改的数据永远可见 - 若某行的
DB_TRX_ID落在中间区间,就顺着DB_ROLL_PTR继续往上翻版本链,直到找到一个DB_TRX_IDm_up_limit_id的节点,或链尾
RC和RR隔离级别差异,全在ReadView创建时机
同一个SQL,在READ COMMITTED下可能每次SELECT都看到新结果,在REPEATABLE READ下却始终不变——这不是缓存,是ReadView复用导致的版本链截断点不同。
-
READ COMMITTED:每次SELECT都新建ReadView→ 能看到其他事务刚提交的版本 -
REPEATABLE READ:仅事务中**第一次**SELECT时创建ReadView,后续复用 → 版本链遍历起点固定,自然“不可重复读”消失 - 注意:
UPDATE/DELETE/SELECT ... FOR UPDATE这类当前读,永远读最新版+加锁,不受ReadView影响
Undo Log版本链不清理会撑爆磁盘,但过早清理又导致查询报错
版本链不是无限存在的。Purge线程负责回收“对所有活跃事务都不可见”的旧版本,但这个节奏一旦失衡,就会出问题。
- 现象:
ERROR 1205 (40001): Deadlock found when trying to get lock或Lock wait timeout exceeded偶发变多 → 可能是Undo Log膨胀导致Purge延迟,进而拖慢版本链遍历 - 现象:
ERROR 1556 (HY000): Cannot use LOCK TABLES when LOCK TABLES is in use或一致性读返回空 → 活跃事务太久(比如长事务没提交),让Purge不敢清理其依赖的旧版本,Undo空间耗尽 - 实操建议:监控
information_schema.INNODB_TRX中TRX_STARTED时间,避免超5分钟的未提交事务;调大innodb_max_purge_lag仅是缓兵之计,根治要靠应用层事务粒度控制



















