暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL 核心模块揭秘 | 49 期 | 更新记录的 Undo 日志

223

作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。
爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。




本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。

目录

  • 1. 准备工作

  • 2. Update Undo 日志格式

  • 3. Update Undo 日志内容

  • 4. Update Undo 日志地址

  • 5. 总结

正文

1. 准备工作

创建测试表:

CREATE TABLE `t6` (
  `id` int unsigned NOT NULL,
  `name` varchar(32DEFAULT '',
  `mobile` char(11DEFAULT '',
  `sex` enum('男','女','未填写'DEFAULT NULL,
  `address` varchar(128DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`),
  KEY `idx_address` (`address`)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入测试数据:

INSERT INTO `t6` (`id``name``mobile``sex``address`VALUES
(1'唐僧''12800128000''男''东土大唐'),
(5'西梁女王''11800118000''女''女儿国'),
(10'张三''13800138000''男''张家口'),
(15'李四''13900139000''男''李家庄'),
(20'王五''15900159000''男''王家大院'),
(25'紫霞仙子''19900199000''女''九龙城'),
(30'猪八戒''16900169000''男''高老庄'),
(35'孙悟空''17900179000''男''花果山'),
(40'沙和尚''18900189000''男''流沙河');

示例 SQL:

UPDATE `t6`
SET `mobile` = '17988179888'`address` = '水帘洞'
WHERE `id` = 35;

2. Update Undo 日志格式

Update 语句更新表中一条记录,先更新主键索引,再更新二级索引。更新二级索引的流程为:遍历二级索引,每轮循环处理一个二级索引,如果当前二级索引包含 Update 语句的 Set 子句中指定的字段,则更新该二级索引,否则,不更新。

更新主键索引记录之前,会生成 Undo 日志,并写入 Undo 页。更新二级索引记录,不会生成 Undo 日志。更新记录产生的 Undo 日志格式,如下图所示。

各属性详细说明如下:

  • next_record_offset,占用 2 字节,表示下一条 Undo 日志在 Undo 页中的偏移量。
  • type_flag,占用 1 字节,表示这条 Undo 日志的类型,以及一些标志位。
  • lob_flag,占用 1 字节,硬编码为 0x00。
  • undo_no,64 位整数,压缩之后占用 1 ~ 11 字节,表示这条 Undo 日志的编号。
  • table_id,64 位整数,压缩之后占用 1 ~ 11 字节。这个属性值是表 ID,表示事务更新哪个表的记录产生了这条 Undo 日志。
  • info_bits,占用 1 字节,表示 InnoDB 记录的头信息中一些标志位。
  • DB_TRX_ID,压缩之后占用 5 ~ 9 字节,表示本次 Update 操作之前,InnoDB 表中记录的 DB_TRX_ID
    字段值。
  • DB_ROLL_PTR,压缩之后占用 5 ~ 9 字节,表示本次 Update 操作之前,InnoDB 表中记录的 DB_ROLL_PTR
    字段值。
  • primary_field_len,压缩之后占用 1 ~ 5 字节,表示主键字段值的长度。
  • primary_field_value,占用 primary_field_len
    字节,存储时不会压缩。
    如果主键是由多个字段组成的联合主键,Undo 日志中,按照联合主键定义的字段顺序,写入所有主键字段的长度和值:
    primary_field_len_1
    primary_field_value_1
    primary_field_len_2
    primary_field_value_2
    ... ...
    primary_field_len_N
    primary_field_value_N
  • n_updated,压缩之后占用 1 ~ 5 字节,表示本次 Update 操作更新了几个字段。
  • upd_field_pos,压缩之后占用 1 ~ 5 字节,表示更新字段在表中的位置。
  • upd_field_len,压缩之后占用 1 ~ 5 字节,表示 Update 语句的 Set 子句中指定的字段,更新之前的字段值的长度(占用字节数)。
  • upd_field_value,占用 upd_field_len
    字节,表示 Update 语句的 Set 子句中指定的字段,更新之前的字段值,存储时不会压缩。
    如果更新了 N 个字段(N >= 2),Undo 日志中,按照 Update 语句的 Set 子句中指定的字段顺序,写入更新字段在表中的位置、更新之前的字段值的长度、更新之前的字段值。如下:
    upd_field_pos_1
    upd_field_len_1
    upd_field_value_1
    upd_field_pos_2
    upd_field_len_2
    upd_field_value_2
    ... ...
    upd_field_pos_N
    upd_field_len_N
    upd_field_value_N
  • index_field_bytes,占用 2 字节,表示 index_field_bytes
    index_field_pos
    index_field_len
    index_field_value
    这个区域占用的总字节数。
  • index_field_pos,压缩之后占用 1 ~ 5 字节,表示二级索引字段在表中的位置。
  • index_field_len,压缩之后占用 1 ~ 5 字节,表示本次 Update 操作之前,二级索引字段值的长度。
  • index_field_value,占用 index_field_len
    字节,表示本次 Update 操作之前,二级索引的字段值。
    如果表中有 N 个字段(N >= 2)属于二级索引,Undo 日志中,按照二级索引字段在表中出现的位置,写入字段值的长度和字段值。如下:
    index_field_pos_1
    index_field_len_1
    index_field_value_1
    index_field_pos_2
    index_field_len_2
    index_field_value_2
    ... ...
    index_field_pos_N
    index_field_len_N
    index_field_value_N
  • current_record_offset,这条 Undo 日志在 Undo 页中的偏移量。

type_flag 属性,表示 Undo 日志的类型,还包含一些标志位,如下。

从图中可以看到,type_flag 分为四部分:

  • 第 1 ~ 4 位,对应 offset 0 ~ 3,表示 Undo 日志的类型。
  • 第 5 ~ 6 位,对应 offset 4 ~ 5。
    如果 offset 4 的值为 1,表示 Update 操作没有更新主键字段、二级索引字段。
    如果 offset 5 的值为 1,表示 Update 操作更新字段时,没有改变字段值的长度,也就是更新之前和更新之后,字段值的长度相同,整条记录的长度自然也就没有改变。
  • 第 7 位,对应 offset 6,如果值为 1,表示这条 Undo 日志中包含 lob_flag 属性。
  • 第 8 位,对应 offset 7,如果值为 1,表示更新了溢出字段。

info_bits 属性,表示本次 Update 操作之前,记录的头信息中第 1 字节的第 5 ~ 8 位(对应 offset 4 ~ 7)的标志位,如下。

  • min_rec_flag,对应 offset 4,如果值为 1,表示该记录是主键索引 B+ 树的非叶子节点中的最小记录。Undo 日志中,info_bits 拷贝自主键索引 B+ 树叶子结点的头信息,此标志为 0。
  • deleted_flag,对应 offset 5,如果值为 1,表示该记录已经被标记删除了,等待事务提交后,后台 purge 线程物理删除该记录。
  • version_flag,对应 offset 6,用于 Online DDL。
  • instant_flag,对应 offset 7,用于 Online DDL。

3. Update Undo 日志内容

示例 SQL 更新 t6 表中 <id = 35>
的记录产生的 Undo 日志,如下图所示。

各属性值详细说明如下:

  • 420,下一条 Undo 日志在 Undo 页中的偏移量。这个值不会压缩,固定占用 2 字节。
  • 76,这个值由 12 | 64
    得到,不会压缩,固定占用 1 字节。
    12
    表示这条 Undo 日志是更新记录产生的,代码里定义为 TRX_UNDO_UPD_EXIST_REC

    64
    表示这条 Undo 日志中包含 lob_flag
    属性,代码里定义为 TRX_UNDO_MODIFY_BLOB
  • 0x00,代码里把 lob_flag 的值硬编码为 0x00,也就是 0。
  • 0,这条 Undo 日志的编号。压缩之后占用 1 字节。
    这个值来源于事务对象的 undo_no
    属性。事务产生的第一条 Undo 日志编号为 0,第二条 Undo 日志编号为 1,依此类推。
  • 1431,这是 t6 表的 ID。压缩之后占用 2 字节。
  • 0,Update 操作之前,t6 表中 <id = 35>
    的记录的头信息中第 1 字节第 5 ~ 8 位的值。
  • 2342,Update 操作之前,t6 表中 <id = 35>
    的记录中 DB_TRX_ID
    字段的值。压缩之后占用 5 字节。
  • 36310272004391275,Update 操作之前,t6 表中 <id = 35>
    的记录的 DB_ROLL_PTR
    字段的值,压缩之后占用 8 字节。
  • 4,主键字段(id)值的长度。压缩之后占用 1 字节。
  • 35,主键字段(id)值。主键字段类型为 int unsigned
    ,占用 4 字节。
  • 2,更新的字段数量。压缩之后占用 1 字节。如果 Update 语句的 Set 子句中指定的字段值,和表中对应字段的值不同,这个属性值就等于 Set 子句中指定的字段数量。
  • 4,更新的第一个字段(mobile)在表中的位置。
  • 11,Update 操作之前,mobile 字段值的长度。压缩之后占用 1 字节。
  • 17900179000,Update 操作之前的 mobile 字段值。
  • 6,更新的第二个字段(address)在表中的位置。
  • 9,Update 操作之前,address 字段值的长度。压缩之后占用 1 字节。
  • 花果山,Update 操作之前的 address 字段值。
  • 30index_field_bytes
    index_field_pos
    index_field_len
    index_field_value
    这个区域(对应图中紫色区域 offset 388 ~ 418)占用的总字节数。
  • 0,二级索引中 id 字段在表中的位置。id 是主键索引字段,同时也是所有二级索引的最后一个字段。
  • 4,Update 操作之前,id 字段值的长度。
  • 35,Update 操作之前的 id 字段值。
  • 3,二级索引 idx_name 中 name 字段在表中的位置。
  • 9,Update 操作之前,name 字段值的长度。
  • 孙语空,Update 操作之前的 name 字段值。
  • 6,二级索引 idx_address 中 address 字段在表中的位置。
  • 9,Update 操作之前,address 字段值的长度。
  • 花果山,Update 操作之前的 address 字段值。
  • 337,这条 Undo 日志在 Undo 页中的偏移量。这个值不会压缩,固定占用 2 字节。

4. Update Undo 日志地址

InnoDB 存储引擎的表中,每条记录都有个隐藏字段 DB_ROLL_PTR
,字段长度固定为 7 字节。通过这个字段值可以找到 Undo 日志(也是 MVCC 中记录的历史版本)。

从整体上来看,我们可以认为它是 Undo 日志的地址。但是,这个字段值实际上由 4 部分组成,如下图所示。

各属性详细说明如下:

  • is_insert,表示这条 Undo 日志是否是插入记录产生的。
  • undo_space_id,这条 Undo 日志所属 Undo 表空间的 ID。
    InnoDB 最多支持 127 个 Undo 表空间,ID 范围是 0 ~ 127。7 bit 可以表示的最大数字正好是 127。
  • page_no,这条 Undo 日志所属 Undo 页的页号。
  • offset,这条 Undo 日志在 Undo 页中的偏移量。

DB_ROLL_PTR 的计算公式如下:

is_insert << 55 | undo_space_id << 48 | page_no << 16 | offset

以示例 SQL 为例,更新记录时产生 Undo 日志得到的各属性值如下:

  • is_insert = false, 转换成整数就是 0。
  • undo_space_id = 2。
  • page_no = 135。
  • offset = 337。

用 Shell 按照以上公式计算得到 DB_ROLL_PTR,如下:

# 输出结果为 562949962269009
echo $((0 << 55 | 2 << 48 | 135 << 16 | 337))

5. 总结

没有需要总结的内容。

往期回顾

Undo 模块(连载中):


事务模块合集 | 锁模块合集





以下是作者的个人公众号和联系方式,欢迎交流。


公众号一树一溪微信csch52

文章转载自爱可生开源社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论