MySQL高并发下undo log版本链回滚,同一行数据回滚的底层细节是如何构建的?

摘要:在MySQL InnoDB高并发写同一行数据的场景中,undo log版本链是保证事务原子性、实现MVCC的核心。当版本链中某条事务回滚时,InnoDB并非简单“删除”该事务的版本记录,而是通过回滚指针(roll_pointer) 逆向遍历
在MySQL InnoDB高并发写同一行数据的场景中,undo log版本链是保证事务原子性、实现MVCC的核心。当版本链中某条事务回滚时,InnoDB并非简单“删除”该事务的版本记录,而是通过回滚指针(roll_pointer) 逆向遍历版本链,将数据恢复到该事务执行前的“基准版本”;同时,回滚操作会影响版本链的结构,但不会破坏其他事务的版本可见性。本文结合高并发写同一行的场景,拆解“事务回滚→版本链回溯→数据恢复”的完整细节。 一、前置基础:undo log版本链的结构(高并发写同一行场景) 1. 版本链的核心构成 InnoDB为每行数据维护3个隐藏列,是版本链的基础: 隐藏列 作用 trx_id 最后修改该行数据的事务ID(递增,唯一标识事务) roll_pointer 回滚指针,指向当前版本对应的undo log记录(形成版本链) db_row_id 行ID(无主键/唯一索引时自动生成,本文不重点关注) 2. 高并发写同一行的版本链生成示例 假设存在一行初始数据:id=1, stock=100(初始版本trx_id=0,roll_pointer=null),高并发下3个事务依次修改该行,生成版本链: -- 事务1(T1,trx_id=1001):stock=100 → 99 BEGIN; UPDATE goods SET stock=99 WHERE id=1; -- 未提交 -- 事务2(T2,trx_id=1002):等待T1锁释放后,stock=99 → 98 BEGIN; UPDATE goods SET stock=98 WHERE id=1; -- 未提交 -- 事务3(T3,trx_id=1003):等待T2锁释放后,stock=98 → 97 BEGIN; UPDATE goods SET stock=97 WHERE id=1; -- 未提交 此时版本链结构(从最新到最旧): T3版本(stock=97, trx_id=1003, roll_pointer→T2 undo log) ↑ T2版本(stock=98, trx_id=1002, roll_pointer→T1 undo log) ↑ T1版本(stock=99, trx_id=1001, roll_pointer→初始版本 undo log) ↑ 初始版本(stock=100, trx_id=0, roll_pointer=null) 关键:每个事务修改数据时,会先写入undo log(记录“反向操作”),再更新数据行的trx_id和roll_pointer,版本链始终“从新到旧”指向历史版本。 二、核心场景:版本链中间事务回滚的操作细节 以上述场景为例,假设事务2(T2)在提交前执行回滚(此时T1未提交、T3未提交),拆解回滚的完整步骤: 场景前提 T1:持有锁→修改stock=99→未提交→持有锁; T2:等待T1锁释放→获取锁→修改stock=98→未提交→持有锁; T3:等待T2锁释放→未获取锁→处于等待状态; 此时执行:T2 → ROLLBACK; 步骤1:触发回滚,定位当前事务的undo log记录 T2执行ROLLBACK后,InnoDB首先根据T2的trx_id=1002,找到该行数据中T2版本对应的undo log记录; T2的undo log是逻辑日志,内容为:表=goods, 行=id=1, 操作类型=UPDATE, 原始值=stock=99, 新值=stock=98, trx_id=1002, roll_pointer→T1 undo log undo log类型:UPDATE操作生成UPDATE_UNDO日志(INSERT生成INSERT_UNDO,DELETE生成DELETE_UNDO)。 步骤2:逆向回溯版本链,恢复数据到“回滚基准版本” T2的回滚目标是“撤销自身修改,将数据恢复到T2执行前的状态”,即T1的版本(stock=99),核心操作: 读取undo log中的原始值:从T2的undo log中提取“修改前的原始值stock=99”; 恢复内存数据页:将Buffer Pool中id=1数据页的stock值从98改回99; 重置数据行的隐藏列: 将数据行的trx_id从1002改回1001(恢复为T1的事务ID); 将数据行的roll_pointer从“指向T2 undo log”改回“指向T1 undo log”; 标记T2的undo log为“可清理”:T2的undo log不再关联到数据行的版本链,后续由purge线程异步清理(需等待无读事务引用)。
阅读全文