十二、MySQL事务日志-undo log
- 12.1 undo log 引入
- 12.2 undo log 的作用
- 01、回滚数据
- 02、MVCC
- 12.3 undo log 的存储结构
- 01、回滚段与 undo 页
- 02、回滚段与事务
- 03、回滚段中的数据分类
- 12.4 undo log 的类型
- 12.5 undo log 的生命周期
- 01、执行 insert 操作
- 02、执行 update 操作
- 03、执行 delete 操作
- 12.3 小结
redo log 是事务持久性的保证,undo log 是事务原子性的保证。
在事务中更新数据的前置操作其实就是要先写入一个 undo log。
12.1 undo log 引入
事务需要保证原子性,也就是说,事务中的操作要么全部成功,要么全部失败。但是有时候事务执行到一半的时候会出现一些情况:
- 情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误、操作系统的错误、甚至突然断电导致的错误等等。
- 情况二:在事务执行的过程中手动执行 rollback 语句结束当前事务的操作。
一旦出现以上的情况,就需要把数据恢复成原先的样子,这个过程就是 "回滚",这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性的要求。
所以,需要重点关注的就是如何 “回滚”:
当我们对一条数据做改动时(这里的改动可以是 insert、delete、update),就把回滚时所需的东西记录下来。
- 插入一条记录时,至少要把这条记录的主键值记录下来,之后回滚的时候只需要把这个主键值对于的记录删除掉就可以了(对于每个 insert,InnoDB 存储引擎会完成一个 delete)。
- 删除一条记录时,至少要把这条记录中的内容都记录下来 ,之后回滚的时候再把由这些内容组成的记录插入到表中就可以了(对于每个 delete,InnoDB 存储引擎会执行一个 insert)。
- 修改一条记录时,至少要把修改这条记录前的旧值都记录下来,之后回滚时再把这条记录更新为旧值就可以了(对于每个 update,InnoDB 存储引擎会执行一个相反的 update,将修改前的行放回去)。
MySQL 把这些为了回滚而记录的这些内容称之为撤销日志或回滚日志。由于查询操作并不会修改任务用户记录,所以在查询操作执行时,并不需要记录相应的 undo log。
另外需要注意的是:undo log 会产生 redo log,也就是 undo log 的产生会伴随着 redo log 的产生,这是因为 undo log 也需要持久性的保护。
12.2 undo log 的作用
01、回滚数据
undo log 是逻辑日志 ,只是将数据库逻辑地恢复到原来的样子。
所有修改都被逻辑地取消了,但是数据结构和页本身在回滚之后可能大不相同 。
这是因为在多用户并发系统中,可能会有数十、数百甚至数千个并发事务。数据库的主要任务就是协调对数据记录的并发访问 。比如,一个事务在修改当前页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。
02、MVCC
undo log 的另一个作用是 MVCC,即在 InnoDB 存储引擎中 MVCC 的实现是通过 undo 来完成。当用户读取一行记录时,如果该记录已经被其他事务占用,当前事务可以通过 undo log 读取之前的行版本信息,以此实现非锁定读取。
12.3 undo log 的存储结构
01、回滚段与 undo 页
回滚段
InnoDB 对 undo log 的管理采用段的方式,也就是回滚段(rollback segment)。
每个回滚段记录了 1024 个 undo log segment,而在每个 undo log segment 中进行 undo 页的申请。
在 InnoDB 1.1 版本之前(不包括 1.1 版本),只有一个 rollback segment,所以支持同时在线的事务限制为 1024,这对绝大多数的应用来说已经够用。
从 1.1 版本开始 InnoDB 支持最大 128 个 rollback segment,所以其支持同时在线的事务限制提高到了 128 * 1024。
undo 页的重用
当开启一个事务的时候,要先去 undo log segment 中找到一个空闲的位置,当有空位的时候,就去申请 undo 页,在这个申请到的 undo 页中进行 undo log 的写入,这一个页的默认大小是 16KB。
为每个事务分配一个页是非常浪费的(除非事务非常长),假设应用的 TPS(每秒处理的事务数目)为 1000,那么 1s 就需要 1000 个页,大概需要 16M 的存储,每分钟大概需要 1G 的存储。如果照这样下去,除非 MySQL 清理的非常勤快,否则随着时间的推移,磁盘空间会增长的非常快,而且很多空间都是浪费的。
于是 undo 页就设计的可以重用了,当事务提交时,并不会立刻删除 undo 页。因为重用,所以这个 undo 页可能混杂着其他事务的 undo log,undo log 在提交后,会被放到一个链表中,然后判断 undo 页的使用空间是否小于 3/4,如果小于 3/4 的话 ,则表示当前的 undo 页可以被重用,那么它就不会被回收,其他事务的 undo log 可以记录在当前 undo 页的后面。由于 undo log 是离散的,所以清理对应的磁盘空间时,效率不高。
02、回滚段与事务
- 每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
- 当一个事务开始的时候,会指定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
- 在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用。
- 回滚段存在于 undo 表空间中,在数据库中可以存在多个 undo 表空间,但同一时刻只能使用一个 undo 表空间。
- 当事务提交时,InnoDB 引擎会做两件事
- 将 undo log 放进特殊的列表中,后续供清除线程进行清除;
- 判断所在的 undo 页是否可重用,若可以则分配给下一个事务使用。
03、回滚段中的数据分类
- 未提交的回滚数据:该数据关联的事务还未提交,为了实现读一致性,该数据不能被其他事务覆盖。
- 已提交但未过期的回滚数据:该数据关联的事务已经提交,但受到 undo retention 参数影响还未过期
- 已提交且已过期的回滚数据:该数据关联的事务已提交且数据保存的时间已经超过 undo retention 的指定时间,属于过期数据。当回滚段满了之后,优先覆盖这部分数据。
事务提交后并不能马上删除 undo log 及 undo log 所在的页。这是因为可能还有其他事务需要通过 undo log 来得到行记录之前的版本。所以 事务提交时将 undo log 放入一个链表中,是否可以最终删除 undo log 及 undo log 所在页由清除线程来判断。
12.4 undo log 的类型
在 InnoDB 存储引擎中,undo log 分为两种类型:
-
insert undo log
insert undo log 是指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),所以该 undo log 可以在事务提交后直接删除。不需要进行清除线程操作。
-
update undo log
update undo log 记录的是对 delete 和 update 操作产生的 undo log。该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待清除线程进行最后的删除。
在 InnoDB 中为了更好的支持并发,InnoDB 的多版本一致性读是采用了基于回滚段的方式。另外,对于更新和删除操作,InnoDB并不是真正的删除原来的记录,而是设置记录的 delete mark 为 1。因此为了解决数据 Page 和 Undo Log 膨胀的问题,需要引入清除线程机制进行回收。
一般来讲我们理解的清除线程可以做如下的工作:
- 清理 del flag 标签的记录
- 清理 undo 的历史版本
- 如果需要进行 undo tablespace 截断。
12.5 undo log 的生命周期
在更新 buffer pool 中的数据之前,需要先将该数据事务开始之前的状态写入到 undo log 中,假设更新到一般出错了,就可以通过 undo log 来回滚到事务开始前。
01、执行 insert 操作
begin;
insert into user(name) values("tom");
插入的数据都会生成一条 insert undo log,并且数据的回滚指针会指向它。undo log 会记录序号、插入主键的列和值等,在进行 rollback 的时候,通过主键直接把对应的数据删除即可。
02、执行 update 操作
begin;
update user set name = 'Sun' where id = 1;
会把老的记录写入新的 undo log,让回滚指针指向新的 undo log,它的 undo no 是 1,并且新的 undo log 会指向老的 undo log。
这时再执行:
update user set id = 2 where id = 1;
对于更新主键的操作,会把原来的数据 deletemark 表示打开,这时并没有真正的删除数据,真正的删除会交给清理线程去判断,然后在后面插入一条新的数据,新的数据也会产生 undo log,并且 undo log 的序号会递增。
每次对数据的变更都会产生一个 undo log,当一条记录被变更多次时,就会产生多条 undo log,undo log 记录的是变更前的日志 ,并且每隔 undo log 的序号是递增的,那么当要回滚的时候,按照序号依次向前推,就可以找到原始数据了。
undo log 是如何回滚的?就上面的例子来说,假设执行 rollback:
- 通过 undo no = 3 的日志把 id = 2 的数据删除;
- 通过 undo no = 2 的日志把 id = 1 的数据 deletemark 还原成 0;
- 通过 undo no = 1 的日志把 id = 1 的数据 name 还原成 Tom;
- 通过 undo no = 0 的日志把 id = 1 的数据删除。
03、执行 delete 操作
-
对于 insert undo log
因为 insert 操作的记录,只对事务本身可见,对其他事务不可见,所以该 undo log 可以在事务提交后直接删除,不需要进行清除线程操作。
-
对于 update undo log
该 undo log 可能需要提供 MVCC 机制 ,因此不能在提交事务时就进行删除。提交时放入 undo log 链表,等待清除线程进行最后的删除。
清除线程的两个主要作用:清理 undo 页和清除 page 里带有 delete_bit 标识的数据行。在 InnoDB 中,
事务中的 delete 操作实际上并不是真正的删除掉数据行,而是一种 delete mark 操作,在记录上标识 delete_bit 而不删除记录 。只是一种 "假删除",做了个标记,真正的删除是由后台清除线程去完成的。
12.3 小结
undo log 是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子。
redo log 是物理日志,记录的是数据页的物理变化,undo log 不是 redo log 的逆过程。