mysql系列8—Innodb的undolog
背景
本文涉及的内容较为底层,做了解即可,是以前学习《高性能mysql》和《mysql是怎样运行的》的笔记整理所得。
undolog设计的初始目的是保证事务的原子性。mysql的修改操作发生后,如果所在的事务未被提交,如mysql服务或者操作系统发送了异常或执行了回滚等情况,事务的已修改操作需被还原。而还原需记录必要的数据,这些数据就是undolog。针对插入、删除、修改操作,undolog需要记录的信息量不同:
[1] 新增: 需要把主键记录下来,回滚时,删除主键对应的记录;
[2] 删除: 保存整条记录,回滚时,重新插入记录;
[3] 修改: 保存修改前后的记录,回滚时使用旧值替换新值。
通过特定的设计,可以减少上述信息量的存储,以提高资源使用效率。如mysql实现删除操作时,会通过delete_mark标记已删除(中间状态),对应的undolog日志中就不需要存储完整的记录。
1.undo日志内容
不同操作类型的undolog存储的数据量不同,为减少undolog存储空间,mysql分别设计了对应的数据存储格式,本章将以Insert为例介绍。
有几个概念需要提前了解。
[1] undo ID: 每条Undo日志对应一个ID。
[2] table ID: 每个表都对应一个ID, 可通过innodb_tables查询表ID信息,如:
mysql> SELECT NAME, TABLE_ID, SPACE, ROW_FORMAT, SPACE_TYPE FROM information_schema.innodb_tables WHERE name='test/t_student';
+----------------+----------+-------+------------+------------+
| NAME | TABLE_ID | SPACE | ROW_FORMAT | SPACE_TYPE |
+----------------+----------+-------+------------+------------+
| test/t_student | 16148 | 15086 | Redundant | Single |
+----------------+----------+-------+------------+------------+
此时表ID为16148.
[3] db_trx_id和db_roll_pointer
每条记录包含3个隐藏列,其中两个是db_trx_id和db_roll_pointer,分别表示事务ID和回滚指针。
db_trx_id(事务ID)表示当前记录被新增或者最后一次修改对应的事务ID。
其中,事务ID是一个不断递增的全局变量;事务只有发送修改时,才会被分配一个事务ID,每个事务的ID全局唯一。
db_roll_pointer(回滚指针)表示指向该条记录对应的undo日志(下文介绍)。
[4] next_record属性
在[mysql系列2—InnoDB数据存储方式]介绍过: mysql表的每条记录都有一个指向下一条记录的指针,记录之间通过next_record形成链表。
1.1 undo日志格式
undo日志格式格式如下图所示:
包含四个部分:
[1] 边界信息
end of record存储本条undo日志的结束位置;start of end存储本条undo日志的起始位置;二者用于确定该条undo日志的边界。
[2] undo type
undo type记录了当前undo日志的类型信息; 不同的类型确定了不同的日志内容存储格式。
[3] undo ID和table ID
undo ID 表示该Undo的全局唯一ID,table ID表示修改操作涉及的表的ID。
[4] 日志信息
存储undo日志信息的数据部分,mysql根据undo type确定日志格式和对应的解析逻辑。Insert、Update、Delete操作在这部分存储格式有所区别。
1.2 insert操作
mysql记录Insert类型的Undo日志,只需要保存主键信息即可,如下图所示:
undo日志部分中存放了主键的各列的长度和实际的数值。
以下结合案例进行说明。
创建一个t_student表:
CREATE TABLE `t_student` (
`id` INT(10) NOT NULL COMMENT '学号,唯一ID',
`name` VARCHAR(50) NOT NULL COMMENT '姓名',
`country` VARCHAR(32) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unix_name` (`name`) USING BTREE
)
ENGINE=InnoDB
;
包含一个主键索引和一个唯一索引;
开启一个事务(事务ID为100),向t_student其中插入两条数据:
start transaction;
INSERT INTO `t_student` (`id`, `name`, `country`) VALUES (1, 'sy', '中国');
INSERT INTO `t_student` (`id`, `name`, `country`) VALUES (2, 'mf', '澳大利亚');
从information_schema.innodb_tables表中查询得到t_student的table ID为16148.
此时数据记录和undo日志如下所示:
两个插入操作对应的类型为trx_undo_insert_rec;undo no存放的是undo日志ID, 分别为100和101;table ID存储的是t_student的table ID为16148; 数据部分存储主键大小和实际的值,整形长度为4;主键数值分别为1和2.
为了下文表述方便,对上图的日志细节部分进行忽略,简化表示为:
图中插入的两条记录通过next_record形成了记录链;每条记录的db_roll_pointer指向了记录对应的Undo日志。
其中:id=1的undo日志ID为100(后续用undolog100表示);id=2的undo日志ID为101(后续用undolog101表示).
1.3 delete操作和update操作
insert和delete对每行记录的操作只会对应产生一条UNDO日志;update语句修改某条记录的主键时产生两条日志,否则产生一条日志。
delete和update操作对应的undo行为是恢复数据,因此需要在日志内容区域存储除主键外更多的信息,其中包括原记录对应的事务ID和Undo指针。以下结合案例进行说明。
当事务100执行插入操作后,继续执行update操作时:
UPDATE t_student SET country = '上海' WHERE id = 2;
update操作修改了主键为2的记录,对应的undo日志ID为102(后续用undolog102表示). undolog102中存在一个old_roll_pointer指针,指向了undolog101.
当事务100执行update操作后,继续执行delete操作时:
delete from t_student WHERE id = 2;
delete操作删除了主键为2的记录,减delete_mark标记为1表示该记录已被删除;对应的undo日志ID为103(后续用undolog103表示). undolog103中存在一个old_roll_pointer指针,指向了undolog102.
说明:也可以提交事务100后(不提交会因行锁冲突阻塞更新),再开启一个事务执行更新或者删除操作,效果与案例相似(仅保存的旧事务ID不别)。
1.4 版本链
insert语句在事务提交后UNDO日志会被删除;而delete和update类型的UNDO日志会参与MVCC,即使所在事务提交,也不会立即删除(而是后续有单独的线程完成清理)。
上一节中id为2的记录对应的undolog通过roll_ptr指针相连,将其单独抽出来得到一个链表,叫做版本链。
2.undo页
前面已经介绍了undo日志格式,undo日志会按照格式存放到FIL_PAGE_UNDO_LOG (undo页)中,undo页的结构如下所示:
File Header和File Trailer是页的固定格式,在介绍数据页时已经介绍过,这里关注一下Undo Page Header, 包括如下4个部分:
[1] type: Undo日志大类,Insert类型和其他类型(delete和update类型);
[2] page_start: 第一条undo日志的起始位置;
[3] page_free: 下一条undo日志的可写入位置;
[4] node信息: 存储链表信息,在下一章中介绍。
3.undo链表
一个事务可能有多个操作命令,每个操作命令可能修改多个行;即一个事务在执行过程中可能生成多个Undo日志,因此一个undo页可能写不完,所以mysql引入了一个undo链的概念。
上一章节中node存储的就是串联undo页之间的指针信息,undo页通过node信息形成双向链表。
mysql整体上将Undo日志分为了两类: insert类型和其他类型(update和delete)。Insert类型的Undo日志在事务提交后可以直接删除;而其他类型的undo日志参与MVCC,不能直接删除,而是由后续专门的线程负责清理。因此,为了提高效率,mysql将不同类型的日志存放到在不同的undo页中。
说明:并不是事务创建时就分配undo链,而是按需分配,发送数据修改时才会分配。
每个事务可能有多条链:
除此之外,对于临时表,还有单独的两个链表(原理相同,本文不做介绍)。
最后,为了提高undo日志写入效率,不同的事务在执行过程中生成的undo日志存放到不同的undo页面链表中;即每个事务有自己单独的undo链表。