20.1 事务回滚的需求
在事务执行的过程中,可能会修改很多东西,为了能够把它们还原到原先的样子(这个过程就是回滚),就需要把所做的修改记录下来,也即 undo log。到回滚时,根据 undo log 记录进行相反的修改,就把数据库还原了,符合原子性的要求。
20.2 事务id
20.2.1 给事务分配id的时机
如果某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储引擎就会给它分配一个独一无二的事务id,分配方式如下:
- 对于只读事务来说,只有在它第一次对某个用户创建的
临时表
执行增、删、改操作时才会为这个事务分配一个事务id,否则不分配。 - 对于读写事务来说,只有在它第一次对某个表(包括临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则不分配。
20.2.2 事务id是怎么生成的
- 服务器维护了一个全局变量,当要分配事务id时,把该变量的值当作事务id分配给该事务,并将该变量自增1
- 每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称为 Max Trx ID 的属性处,这个属性占用8个字节的存储空间
- 当系统下一次重启时,会将 Max Trx ID 加载到内存中,将该值加上256后赋值给那个全局变量,这样就可以保证整个系统中分配的事务id是递增的。
20.2.3 trx_id隐藏列
聚簇索引的记录除了会保存完整的用户数据以外,会自动添加名为 trx_id、roll_pointer 的隐藏列。如果用户没有在表中定义主键以及 UNIQUE 键,还会自动添加一个名为 row_id 的隐藏列。
22.3 undo log 的格式
为了实现事务的原子性,InnoDB 在实际进行增、删、改操作时,都需要先把对应的 undo log 记录下来。
这些 undo log 是被记录到类型为 FIL_PAGE_UNDO_LOG 的页面中的。
CREATE TABLE undo_demo (id INT NOT NULL,key1 VARCHAR(100),col VARCHAR(100),PRIMARY KEY (id),KEY idx_key1 (key1)
)Engine=InnoDB CHARSET=utf8;
SELECT * FROM information_schema.INNODB_TABLES WHERE name = 'learning/undo_demo';
PS:在 MySQL 8.0.3 及更高版本中,一些过时的的 InnoDB 系统表相关的代码被移除,所以某些旧版本的系统表不再是一个有效的 INFORMATION_SCHEMA 表。
基于 InnoDB 系统表的 INFORMATION_SCHEMA 视图被内部数据字典表上的系统视图所取代,受影响的 INFORMATION_SCHEMA 视图也被重命名。
以下是 InnoDB INFORMATION_SCHEMA 视图的旧名称和新名称的对应关系:
旧名称 | 新名称 |
---|---|
INNODB_SYS_COLUMNS | INNODB_COLUMNS |
INNODB_SYS_DATAFILES | INNODB_DATAFILES |
INNODB_SYS_FIELDS | INNODB_FIELDS |
INNODB_SYS_FOREIGN | INNODB_FOREIGN |
INNODB_SYS_FOREIGN_COLS | INNODB_FOREIGN_COLS |
INNODB_SYS_INDEXES | INNODB_INDEXES |
INNODB_SYS_TABLES | INNODB_TABLES |
INNODB_SYS_TABLESPACES | INNODB_TABLESPACES |
INNODB_SYS_TABLESTATS | INNODB_TABLESTATS |
INNODB_SYS_VIRTUAL | INNODB_VIRTUAL |
20.3.1 INSERT操作对应的undo log
日志类型: TRX_UNDO_INSERT_REC
- undo no 在一个事务中是从0开始递增的。
- len = 列占用的存储空间;value = 列的真实值。
插入两条记录:
BEGIN; # 显式开启一个事务,假设该事务的id为100# 插入两条记录
INSERT INTO undo_demo(id, key1, col)
VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
产生的两条 undo log 如下:
20.3.1.1 roll pointer隐藏列的含义
roll pointer 本质上就是一个指向对应 undo log 的指针。
20.3.2 DELETE操作对应的undo log
- 正常记录链表:页面中正常的记录使用记录头信息中的 next_record 属性组成的单向链表
- 垃圾链表:被删除的记录使用记录头信息中的 next_record 属性组成的单向链表
使用 DELETE 语句删除数据时,分为两个阶段:
- 将记录的 delete_mask 标识设为1,其他不做修改。此阶段称为 delete mark。
- 事务提前之后,会有专门的线程把记录从
正常记录链表
移动到垃圾链表
,并调整页面其他信息,如 PAGE_N_RECS、PAGE_LAST_INSERT、PAGE_FREE、PAGE_GARBAGE 等。此阶段称为 purge。
也就是说,回滚时,只需要考虑对 delete mark 阶段进行即可。
日志类型:TRX_UNDO_DEL_MARK_REC
-
在对一条记录进行 delete mark 操作前,需要把该记录的旧的 trx_id 和 roll_pointer 隐藏列的值都记到对应的 undo log 中来。这样就可以通过 undo log 中的 old roll_pointer 找到记录在修改之前对应的 undo log。比如在一个事务中,先插入一条数据,再将它删除:
-
索引列各列信息:如果某个列被包含在某个索引中,那么它的相关信息就应该会被记录。pos = 该列在记录中的位置;len=该列占用的存储空间;value=该列实际值。
删除 id=1 的数据:
BEGIN; # 显式开启一个事务,假设该事务的id为100# 插入两条记录
INSERT INTO undo_demo(id, key1, col)
VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');# 删除一条记录
DELETE FROM undo_demo WHERE id = 1;
PS:关于上述各个属性,详见[5.5 Page Header(页面头部)](###5.5 Page Header(页面头部))
20.3.3 UPDATE操作对应的undo log
20.3.3.1 不更新主键的情况
-
就地更新(in-place-update)
直接在原记录的基础上修改对应列的值,每个列在更新前后占用的存储空间一样大,有任何一个列更新前后占用空间发生变化都不能进行就地更新。
-
先删除旧记录,再插入新记录
当被更新列中有更新前后占用空间发生变化时,那么就需要先把这条旧的记录从聚簇索引中删掉,再插入一条新的记录。注意这里的删除是真实删除,也就是把要删除的记录从正常记录链表移动垃圾链表,并且修改页面中相应的统计信息。并且是由用户线程同步执行的。
日志类型:TRX_UNDO_UPD_EXIST_REC
BEGIN; # 显式开启一个事务,假设该事务的id为100# 插入两条记录
INSERT INTO undo_demo(id, key1, col)
VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');# 删除一条记录
DELETE FROM undo_demo WHERE id = 1;# 更新一条记录
UPDATE undo_demo
SET key1 = 'M249', col = '机枪'
WHERE id = 2;
此时采用的是就地更新
的方式执行,真正发动页面记录前,会记录一条类型为TRX_UNDO_UPD_EXIST_REC
的 undo log。
20.3.3.2 更新主键的情况
分为两步处理:
- 对旧记录进行
delete mark
操作,并记录一条TRX_UNDO_DEL_MARK_REC
的 undo log。 - 根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中(需要重新定位插入的位置),记录一条
TRX_UNDO_INSERT_REC
的 undo log。
20.3.4 增删改操作对二级索引的影响
UPDATE undo_demo
SETkey1 = 'P92',col = '手枪'
WHEREid = 2;
由于更新了二级索引的键值,所以要进行以下两个操作:
- 对旧的二级索引记录执行
delete mark
操作 - 根据更新后的值创建一条新的二级索引记录,并在 B+ 树中重新定位插入
此操作会修改当前页的最大事务id PAGE_MAX_TRX_ID
属性。
20.3.5 小结
20.4 通用链表结构
20.5 FIL_PAGE_UNDO_LOG页面
FIL_PAGE_UNDO_LOG 类型页面是专门用来存储 undo log 的页面类型。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
属性 | 占用空间(byte) | 含义 |
---|---|---|
TRX_UNDO_PAGE_TYPE | 2 | 存储什么种类的 undo log。TRX_UNDO_INSERT(十进制表示为1)对应 TRX_UNDO_INSERT_REC 类型的 undo log;TRX_UNDO_UPDATE(十进制表示为2)对应除TRX_UNDO_INSERT_REC 类型的 undo log。不同类型的 undo log 不能混着存储。 |
TRX_UNDO_PAGE_START | 2 | 表示在当前页面中是什么位置开始存储 undo log 的,也即第一条 undo log 在本页面中的起始偏移量。 |
TRX_UNDO_PAGE_FREE | 2 | 表示当前页面最后一条 undo log 在本页面中的起始偏移量,也即从这个位置开始可以继续写入新的 undo log。 |
TRX_UNDO_PAGE_NODE | 12 | 代表一个 List Node 结构。 |
20.6 Undo页面链表
20.6.1 单个事务中的Undo页面链表
一个事务可能包含多个语句,更新多条记录,产生多条 undo log,一个页面放不下时需要放到多个页面中,这些页面就通过上述的 TRX_UNDO_PAGE_NODE
属性形成一个链表:
第一个链表节点称为 first undo page;其余的则称为 narmal undo page。
因为 TRX_UNDO_INSERT
和 TRX_UNDO_UPDATE
不能存在同一个页面,所以这样的链表其实有两条:
又因为 InnoDB 规定对普通表的临时表的记录发动时产生的 undo log 要分别记录,所以一个事务中最多有4个由 Undo 页面组成的链表:
这些链表并不是事务一开始就存在的,而是按需分配,啥时候需要再分配,不需要就不分配。
20.6.2 多个事务中的Undo页面链表
为了提高 undo log 的写入效率,不同事务执行过程中产生的 undo log 需要被写入到不同的 Undo Page 链表中。
20.7 undo日志具体写入过程
20.7.1 段(Segemnt)的概念
段是一个逻辑上的概念,本质上是由若干个零散页面和若干个完整的区组成。每个段对应一个 INODE Entry 结构,描述这个段的各种信息,如段的 ID,段内各种链表基节点,零散页面的页号等。为了定位一个 INODE Entry,InnoDB 提供了 Segemnt Header 结构:
- Space ID of the INODE Entry:INODE Entry 结构所在的表空间 ID
- Page Number of the INODE Entry:INODE Entry 结构所在的页面页号
- Byte Offset of the INODE Entry:结构在该页面中的偏移量
20.7.2 Undo Log Segemnt Header
InnoDB 规定每个 Undo 页面链表都对应着一个段,称为 Undo Log Segemnt。其中 first undo page 中设计了一个称为 Undo Log Segemnt Header 的部分:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
-
TRX_UNDO_STATE :本 Undo Page 链表处在什么状态。
一个 Undo Log Segment 可能处在的状态包括:
-
TRX_UNDO_ACTIVE :活跃状态,也就是一个活跃的事务正在往这个段里边写入 undo日志
-
TRX_UNDO_CACHED :被缓存的状态。处在该状态的 Undo页面 链表等待着之后被其他事务重用。
-
TRX_UNDO_TO_FREE :对于 insert undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。
-
TRX_UNDO_TO_PURGE :对于 update undo 链表来说,如果在它对应的事务提交之后,该链表不能被重用,那么就会处于这种状态。
-
TRX_UNDO_PREPARED :包含处于 PREPARE 阶段的事务产生的 undo日志 。
-
-
TRX_UNDO_LAST_LOG :本 Undo Page 链表中最后一个 Undo Log Header 的位置。
-
TRX_UNDO_FSEG_HEADER :本 Undo Page 链表对应的段的 Segment Header 信息。
-
TRX_UNDO_PAGE_LIST : Undo Page 链表的基节点。
20.7.3 Undo Log Header
同一个事务向一个 Undo Page 链表写入的 undo log 是一个组,不同的链表就是不同的组。
- TRX_UNDO_TRX_ID :生成本组 undo log 的事务 id 。
- TRX_UNDO_TRX_NO :事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。
- TRX_UNDO_DEL_MARKS :标记本组中有没有 Delete mark 操作产生的 undo log。
- TRX_UNDO_LOG_START :表示本组第一条日志在页面中的偏移量。
- TRX_UNDO_XID_EXISTS :本组是否包含XID信息。
- TRX_UNDO_DICT_TRANS :标记本组 undo log 是不是由 DDL 语句产生的。
- TRX_UNDO_TABLE_ID :如果 TRX_UNDO_DICT_TRANS 为真,那么本属性表示DDL语句操作的表的 table id 。
- TRX_UNDO_NEXT_LOG :下一组的 undo log 在页面中开始的偏移量。
- TRX_UNDO_PREV_LOG :上一组的 undo log 在页面中开始的偏移量。一般来说一个 Undo Page 链表只存储一个事务执行过程中产生的一组 undo log,但是在某些情况下,可能会在一个事务提交之后,之后开启的事务重复利用这个 Undo Page 链表,这样就会导致一个 Undo Page 中可能存放多组 undo log,TRX_UNDO_NEXT_LOG和TRX_UNDO_PREV_LOG就是用来标记下一组和上一组undo日志在页面中的偏移量的。关于什么时候重用Undo页面链表。
- TRX_UNDO_HISTORY_NODE :一个12字节的 List Node 结构,代表一个称之为 History 链表的节点。
20.7.4 小结
-
对于没有被重用的 Undo Page 链表来说,链表的 first undo page 在真正写入 undo log 前,会填充 Undo Page Header 、 Undo Log Segment Header 、 Undo Log Header 这三个部分,之后才开始正式写入 undo log 。而对于 normal undo page 只会填充 Undo Page Header 两个部分。
-
链表的 List Base Node 存放到 first undo page 的 Undo Log Segment Header 部分, List
Node 信息存放到每一个 Undo页面 的 undo Page Header 部分。
20.8 重用Undo页面
在事务提交后,某些情况下可以重用事务的 Undo Page 链表。重用条件如下:
- 该链表中只包含一个 Undo Page。
- 该 Undo Page 已经使用的空间不足3/4
20.9 回滚段
20.9.1 回滚段的概念
一个事务在执行过程中最多可以分配4个 Undo Page 链表,使用 Rollback Segemnt Header 页面来管理这些链表。 Rollback Segemnt Header 存放了各个 Undo Page 链表的 first undo page 的页号。
InnoDB 规定,每个 Rollback Segemnt Header 页面对应一个段,这个段就称为 Rollback Segemnt,即回滚段。
- TRX_RSEG_MAX_SIZE :本 Rollback Segment 中管理的所有 Undo页面 链表中的 Undo页面 数量之和的最大值。换句话说,本 Rollback Segment 中所有 Undo页面 链表中的 Undo页面 数量之和不能超过TRX_RSEG_MAX_SIZE 代表的值。该属性的值默认为无限大(4字节能表示的最大数)。
- TRX_RSEG_HISTORY_SIZE : History 链表占用的页面数量。
- TRX_RSEG_HISTORY : History 链表的基节点。
- TRX_RSEG_FSEG_HEADER :本 Rollback Segment 对应的10字节大小的 Segment Header 结构,通过它可以找到本段对应的 INODE Entry 。
- TRX_RSEG_UNDO_SLOTS :各个 Undo 页面链表的 first undo page 的 页号 集合,也就是 undo slot 集合。
一个页号占用 4 个字节,对于 16KB 大小的页面来说,这个 TRX_RSEG_UNDO_SLOTS 部分共存储了 1024 个
undo slot ,所以共需 1024 × 4 = 4096 个字节
20.9.2 从回滚段中申请Undo Page链表
- 初始状态下,由于未向任何事务分配任何 Undo Page 链表,所以 Rollback Segemnt Header 的各个 undo slot 都被设置成一个特殊的值:FIL_NULL(对应的十六进制:0xFFFFFFFF)
- 当需要分配 Undo Page 链表时,就从第一个非 FIL_NULL 的 undo slot 开始,如果没有(最多1024个),就报错:Too many active concurrent transactions
- 当事务提交时,它所占用的 undo slot 有两种命运:
- 如果符合重用条件,就被加到 insert undo cached 链表或 update undo cached 链表(根据他们原来的 Undo Page 链表的类型)
- 如果不符合重用条件,根据 Undo Page 链表的类型不同,会被直接释放或加到 History 链表。
20.9.3 多个回滚段
一个回滚段只有 1024 个 undo slot,也就是说即使一个事务只分配1个 Undo Page 链表,那也只能运行 1024 个读写事务同时执行。
为了解决这个问题, InnoDB 定义了128个回滚段。
每个回滚段对应着一个 Rollback Segemnt Header 页面,这128个页面被存储在系统表空间的第5号页面。
20.9.4 回滚段的分类
两类:
- 第0号、第33-127号属于一类
- 第1~32号属于一类
针对普通表和临时表划分不同种类回滚段的原因:在修改针对普通表的回滚段中的 Undo Page 时,需要记录对应的 redo log,而修改针对临时表的回滚段中的 Undo Page 时,不需要记录对应的 redo log。
20.9.5 为事务分配Undo Page链表详细过程
如果一个事务在执行过程中即对普通表的记录做了改动,又对临时表的记录做了改动,那么需要为这个记录分配2个回滚段。并发执行的不同事务其实也可以被分配相同的回滚段,只要分配不同的 undo slot 即可。
20.10 回滚段相关配置
20.10.1 配置回滚段数量
innodb_rollback_segments:回滚段数量,默认128。
20.10.2 配置undo表空间
- innodb_undo_directory:指定 undo 表空间所在的目录
- innodb_undo_tablespaces:指定 undo 表空间的数量,默认0
20.11 undo log在崩溃恢复时的作用
- 服务器崩溃恢复时,首先按照 redo log 将各个页面的数据恢复到崩溃之前的状态,这样就保证了已经提交的事务的持久性
- 但是因为有些没有提交的事务写的 redo log 可能已经刷盘了,所以会把它们也恢复,为了保证事务的原子性,需要把这些操作回滚
- 通过表空间的第5个页面定位到128个回滚段的位置
- 找到每个段中不为 FIL_NULL 的 undo slot 对应的 Undo Page 链表
- 从 Undo Page 链表第一个页面的 Undo Segemnt Header 中找到 TRX_UNDO_STATE 属性,如果该属性为 TRX_UNDO_ACTIVE,意味着有一个活跃的事务正在向这个链表写入 undo log。
- 再在 Undo Segemnt Header 中找到 TRX_UNDO_LAST_LOG 属性,该属性指向的事务id就是未提交的事务,把这个事务所有操作回滚,就保证了事务的原子性
20.12 总结
- undo log 记录了回滚一个操作所有的必要内容,保证了事务的原子性
- 不同的场景有不同的 undo log 类型
- 类型为 FIL_PAGE_UNDO_LOG 的页面是专门用来存储 undo log 的,称为 Undo Page。
- 一个事务最多分配4个链表
- 每个 Undo Page 链表都对应一个 Undo Log Segemnt
- 同一个事务向一个 Undo Page 链表中写入的 undo log 算一个组
- Undo Page 链表可以被重用
- 每一个 Rollback Segemnt Header 页面都对应一个回滚段,包含1024个 undo slot,每个 undo slot 代表一个 Undo Page 链表的第一个页面的页号