正文
创建测试表:
CREATE TABLE `t1` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`i1` int DEFAULT '0',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_i1` (`i1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
插入测试数据:
INSERT INTO `t1` (`id`, `i1`) VALUES
(10, 101), (20, 201),
(30, 301), (40, 401);
示例 SQL:
/* 1 */ begin;
/* 2 */ insert into t1(id, i1)
values(50, 501);
/* 3 */ savepoint savept1;
/* 4 */ insert into t1(id, i1)
values(60, 601);
/* 5 */ savepoint savept2;
/* 6 */ update t1 set i1 = 100
where id = 10;
/* 7 */ savepoint savept3;
/* 8 */ insert into t1(id, i1)
values(70, 701);
/* 9 */ rollback to savept2;
每条 SQL 前面的数字是它的编号,9 条 SQL 分别为 SQL 1、SQL 2、...、SQL 9,其中,SQL 9 是本文的主角。
SQL 2 插入记录 <id = 50, i1 = 501> 产生的 undo 日志编号为 0。
SQL 4 插入记录 <id = 60, i1 = 601> 产生的 undo 日志编号为 1。
SQL 6 更新记录 <id = 10> 产生的 undo 日志编号为 2。
SQL 8 插入记录 <id = 70, 701> 产生的 undo 日志编号为 3。
每个用户线程都有一个 m_savepoints
链表,用户每创建一个 savepoint,它的对象都会追加到链表末尾。
每个 savepoint 对象的 prev 属性都指向在它之前创建的那个 savepoint 对象,多个 savepoint 对象通过 prev 属性连接成链表。
m_savepoints 链表的指针,指向最新加入的 savepoint 对象。
示例 SQL 中,SQL 3、5、7 分别创建了 savept1
、savept2
、savept3
,这 3 个 savepoint 对象形成的 m_savepoints 链表如下:
要回滚到某个 savepoint,第 1 步就是根据名字找到它,因为它里面保存着两个重要数据:
m_savepoints 链表的指针指向最新加入的 savepoint 对象,查找过程自然就是从后往前了。
从后往前遍历 m_savepoints 链表的过程中,如果当前遍历的 savepoint 对象名字等于要回滚的那个 savepoint 对象名字,就找到了,否则,继续往前遍历。
如果遍历完 m_savepoints 链表都没有找到目标 savepoint 对象,就会报错:
(1305, 'SAVEPOINT xxx does not exist')
回滚到某个 savepoint 的过程中,binlog 回滚就是把创建该 savepoint 之后执行 SQL 产生的 binlog 日志都丢弃。
事务提交之前,产生的 binlog 日志都临时存放于 trx cache
,而 trx cache 包含内存 buffer 和磁盘临时文件两部分。
trx cache 中的 binlog 日志,可能有一部分在内存 buffer 中,另一部分在磁盘临时文件中。
基于此,丢弃 binlog 日志可以分为两种情况:
情况 1:丢弃内存 buffer 中的部分或全部 binlog 日志。
这种情况比较简单,不涉及到磁盘临时文件。
trx cache 用 IO_CACHE 来管理内存 buffer 和磁盘临时文件。
IO_CACHE 有个 write_pos
属性,这是个指针,指向新产生的 binlog 日志要写入内存 buffer 中的哪个位置。
binlog 回滚,只需要把 write_pos 往回移动,write_pos 新位置和旧位置之间的那些 binlog 日志就被丢弃了。
那么,write_pos 要往回移动到哪个位置呢?
IO_CACHE 还有个 pos_in_file
属性,这是个整数值,我们也可以把它看成指针,指向内存 buffer 写满之后,里面的内容转移到磁盘临时文件中的哪个位置。
savepoint 中保存着它创建的那一时刻的 binlog offset
,binlog offset 减去 pos_in_file 就是 write_pos
要往回移动到的位置。
情况 2:丢弃内存 buffer 中的全部 binlog 日志,同时还要丢弃磁盘临时文件中的部分或全部 binlog 日志。
这种情况要分两步走:
回滚之前,各指针位置如下图所示:
回滚之后,各指针位置如下图所示:
SQL 9 回滚到 savept2 的过程中,binlog 回滚只需要丢弃内存 buffer 中的部分 binlog 日志,也就是对应情况 1。
事务执行过程中,改变(插入、更新、删除)表中的每条数据,都会对应产生一条 undo 日志。
回滚到某个 savepoint 的过程中,InnoDB 回滚,就是按照 undo 日志产生的时间,从后往前读取 undo 日志。
每读取一条 undo 日志之后,解析 undo 日志,然后执行产生这条日志的操作的反向操作,也就是回滚。
如果某条 undo 日志是插入操作产生的,反向操作就是删除。
如果某条 undo 日志是更新操作产生的,更新操作把字段 A 的值从 101 改为 100,反向操作就是把字段 A 的值从 100 改回 101。
如果某条 undo 日志是删除操作产生的,反向操作就是把记录再插回到表里。
那么,回滚到哪条 undo 日志才算完事呢?
savepoint 中,保存着它创建之前,最后产生的那条 undo 日志的编号,回滚到这条 undo 日志的下一条 undo 日志就完事了。
以 SQL 5 为例,创建 savept2 之前,最后一条 undo 日志是插入记录 <id = 60, i1 = 601> 产生的,编号为 2。
SQL 9,rollback to savept2
回滚到编号为 2 的 undo 日志的下一条 undo 日志(编号为 3)就完事了。
SQL 9 需要回滚编号为 3、4 的两条 undo 日志。以回滚主键索引记录为例,过程如下:
<id = 70>
。<id = 10, i1 = 101>
,101 是 id = 10 的记录被修改之前的 i1 字段值。执行完 binlog 和 InnoDB 的回滚操作之后,还需要删除该 savepoint 之后创建的其它 savepoint。
示例 SQL 中,SQL 5 创建 savept2 之后,SQL 7 又创建了 savept3。
SQL 9 回滚到 savept2,执行完 binlog 和 InnoDB 的回滚操作之后,savept3 就没用了,会被删除。
删除 savept3 之后,m_savepoints 链表如下图所示:
回滚到某个 savepoint,首先要从 m_savepoints 链表中找到这个 savepoint。
找到之后,根据其中保存的 binlog offset、undo 日志编号,执行 binlog 和 InnoDB 的回滚操作。
binlog 回滚就是丢弃 binlog offset 之后的 binlog 日志。
InnoDB 回滚就是根据产生时间,从后往前读取并解析 undo 日志,执行 undo 日志对应的回滚操作。
最后,就是删除在这个 savepoint 之后创建的其它 savepoint。
本期问题:关于本期内容,如有问题,欢迎留言交流。
下期预告:MySQL 核心模块揭秘 | 14 期 | 回滚整个事务。