Undo Log:事务回滚是怎么实现的?
回滚要解决什么事务执行到一半报错了或者用户主动敲了ROLLBACK已经改掉的那些数据得变回去。怎么变回去有两条路改之前先把整个页或者整行拷一份回滚的时候原样写回去记下这次修改的反向操作是什么回滚的时候照着做一遍InnoDB 选的是第二条。undo log 里记的不是旧数据长什么样而是怎么把这次修改撤掉这个区别很关键后面讲 MVCC 复用的时候会用到。undo log 记的是什么三种写操作记的东西不一样操作undo log 里记的回滚时做什么INSERT主键还有唯一索引的值按主键把这一行删掉DELETE整行的所有列 主键把这一行重新插回去UPDATE被改字段的旧值 主键把字段改回旧值INSERT是最省的它不需要记旧值因为原来根本没有这一行记个主键就够回滚用了。DELETE反过来最费行都没了重建它得知道每一列是什么所以整行都得记。这也是为什么DELETE产生的日志量比INSERT大。UPDATE有个特殊情况如果更新的是主键InnoDB 会把它拆成DELETEINSERT两步。因为主键决定了记录在页里的位置改了主键相当于这条记录挪了地方不是原地改几个字节能解决的。这里要强调 undo log 是逻辑日志它记的是把 id 1 这行的 name 改回 ‘a’“不是把页号 5 偏移 100 处的字节恢复成原来的值”。为什么不能做物理恢复因为一个数据页上可能同时躺着好几个事务的修改页是共享的。如果回滚事务 A 的时候把整个页按快照写回去事务 B、C 在同一页上的修改会被一起抹掉。只有按事务维度做逻辑的反向操作才能只撤自己的那一份。undo log 本身也要写 redo log这个地方容易绕进去undo log 不是凭空存在的它也存在 undo 页里而 undo 页是要持久化的。所以一个UPDATE实际发生的是1. 先把 undo 记录写进 undo 页 这次对 undo 页的修改会产生 redo log 2. 再修改数据页 这次对数据页的修改也会产生 redo log也就是说redo log 保护的不只是数据页还有 undo 页。崩溃恢复的时候如果 undo 页的修改也没来得及落盘照样能从 redo log 里把它重放出来然后才能拿它去回滚别的事务。这里的重放具体怎么做可以回顾一下 Redo LogMySQL 为什么能够实现崩溃恢复里的恢复流程。这也解释了一个看起来矛盾的顺序问题undo 明明是先写的为什么还要靠 redo 保护因为 redo 保护的是undo 这个写入动作本身不会丢两件事不冲突。两个用途回滚是本职undo log 的存在意义是回滚MVCC 的读历史版本是顺带复用的同一份数据。具体的机制在 MVCC 的版本链里讲过每次修改前把旧值写进 undo log用DB_ROLL_PTR把同一个行的各个版本串成链表事务读的时候顺着链找到自己可见的那个版本。和回滚的关系可以用一句话概括回滚是沿着版本链往回走MVCC 是沿着版本链往回找。走的是同一条链目的地不一样。什么时候可以删undo 记录不是提交完就能删的。两种 undo类型来源提交后能不能立刻删insert undo logINSERT能因为它产生的行本来就只对当前事务可见update undo logUPDATE、DELETE不能它是版本链的来源别的事务可能还要读insert undo log之所以能立刻删是因为它记录的那行数据是当前事务造出来的事务没提交之前别人根本看不见提交之后这条 undo 记录也就没有用了。update undo log不行。假设事务 A 把name从a改成了b并提交事务 B 在 A 提交之前就开了它读这行的时候要看到a所以记录着旧值a的那条 undo 记录必须留着直到 B 结束。purge 线程怎么判断清理这件事由后台的 purge 线程做判断依据是再没有事务需要看这个版本了。InnoDB 里维护着当前所有活跃事务的信息purge 线程拿 undo 记录归属的事务 ID 去和活跃事务列表比如果这个 ID 比所有活跃事务的 ID 都小说明没有任何一个还没结束的事务会需要比它更早的版本了这条记录就可以清理。清理的时候不只是把 undo 页回收还要把数据页上对应的那条delete mark记录真正删掉DELETE在 InnoDB 里其实是打个删除标记真正的物理删除也是 purge 干的。长事务为什么危险一个事务开了很久不提交会有两个后果第二个才是真正麻烦的。一是它自己的 undo 清理不掉。这个好理解事务还没结束它改过的东西都还不能确定最终是什么样。二是它会挡住别人的 undo 清理。purge 只能清理比活跃事务更早的版本而长事务作为一个一直活着的旧事务会把这条线死死钉在原地。它后面产生的所有 undo 记录哪怕那些事务早就提交了都得排队等着。所以线上 undo 表空间从几百 MB 涨到几十 GB往往不是因为单个大事务而是因为某个忘了提交的连接一直挂着。排查的时候看information_schema.innodb_trx里trx_started时间很早的那些记录基本就是这个原因。