Skip to main content

mysql

📅 2026-03-19 ✏️ 2026-03-19 CS INFRA
No related notes

1 · mysql#

S 关系型数据库,事务数据存储,OLTP场景 C 海量数据下的高并发读写、数据一致性、性能优化 Q 如何理解MySQL的架构、存储引擎、索引、事务、锁机制 A 从架构到存储到查询优化到事务机制,逐层理解

1.1 · 架构

客户端/服务端架构;mysql/mysqld 通信方式:TCP/IP、Unix域套接字

Server层 + 存储引擎层

Server: 连接器、查询缓存、分析器、优化器、执行器;所有的内置函数;所有跨存储引擎的功能(存储过程、触发器、视图…) 存储引擎:负责数据的存储和提取;插件式的,支持 InnoDB(default)、MyISAM、Memory 等多个存储引擎

NOTE:不同的表可以设置不同引擎

连接器:跟客户端建立连接、获取权限、维持和管理连接(成功建立连接后,用户的权限已经确定,修改不会影响此次连接) 长连接: 连接成功后,如果客户端持续有请求,则一直使用同一个连接 短连接: 每次执行完很少的几次查询就断开连接,下次查询再重新建立一个 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放;可能导致内存占用太大(定时断开长连接,或reset连接)

查询缓存:

分析器:词法分析、语法分析

优化器:索引、多表关联的连接顺序

执行器:判断执行权限,根据表引擎定义而使用引擎接口;

1.1.1 · 启动选项、配置文件

系统变量作用范围:全局、会话 状态变量…:

1.1.2 · 字符集

二进制与字符的映射关系;

mysql四个级别的字符集:服务器、数据库、表、列

客户端请求过程中会发生字符集转换;

1.1.3 · 数据目录

数据库、表、视图、触发器等

数据库对应一个子目录;

表分为表定义、表数据: InnoDB:表名.frm描述表结构;表空间存储表数据,表名.ibd; MyISAM:表名.frm;表名.MYI为索引文件,表名.MYD为数据文件;

视图:视图名.frm

1.2 · InnoDB 存储引擎#

https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html version 5.7.44

1.2.1 · 内存架构 + 磁盘结构

https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html

内存:

  • Buffer Pool(Change Buffer):缓冲池,减少磁盘访问;变更缓存池用于二级索引;
  • Adaptive Hash Index:自适应哈希索引,加速频繁访问的数据;
  • Log Buffer:日志缓冲区;

磁盘:

  • System表空间(ibdata1):储存数据字典、双写缓存区、变更缓存池、undo日志;
  • 一表一文件表空间(t1.ibd, tN.ibd)
  • Undo表空间
  • Redo日志(循环写 ib_logfile0, ib_logfile1)
  • 通用表空间、临时表空间

表和索引定义在 *.frm 文件,表数据和索引数据在 *.ibd 文件

双写缓存区: 在数据写入磁盘之前先写(顺序写)入一个”副本”,即使在写入过程中发生意外,有完整副本数据

1.2.1.1 · Buffer Pool#

控制块+缓存页

LRU:分为young和old两个区域

buffer pool chunk

1.2.2 · 行格式

将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,页一般大小为16KB;

InnoDB行格式:Compact, Redundant, Dynamic, Compressed

隐藏列:DB_ROW_ID(无自定义主键时,由引擎添加) DB_TRX_ID DB_ROLL_PTR

1.2.2.1 · Compact#

  1. 额外信息 变长字段长度列表:数据大小不固定:字节数,按照列顺序逆序存放; NULL值列表:使用二进制位表示是否该列值为NULL,按照列顺序逆序排列; 头信息:固定5字节;不同位表示不同信息:

对于 CHAR(M):M表示字符数;使用定长字符集,则不会加入变长字段长度列表;反之使用变长字符集时,则会加入;

  1. 真实数据

1.2.2.2 · Redundant#

mysql5之前使用

  1. 额外信息 字段长度偏移列表:所有列的长度信息按照逆序储存;使用两个数之间的差值进行计算长度; 头信息:固定6字节,

处理NULL值,偏移值为0

对于 CHAR(M):字符数M乘以一个字符的最大字节数;最大字节数不能超过65535;

  1. 真实数据

1.2.2.3 · Dynamic和Compressed#

5.7默认 Dynamic 格式

与Compact相似,但是真实数据全部储存到其他页,只记录地址;

而Compressed会采用压缩算法对页面进行压缩;

1.2.2.4 · 行溢出

页为16KB,16384字节,储存不下65532字节,所以可能造成一个页放不下一条记录; #Compact#Redundant真实数据只会存一部分,剩下的用其他页储存,用真实数据中的20个字节指向其他页; 行溢出临界点:mysql规定一个页中至少两行记录

1.2.3 · 页

类型:存表头部信息、InsertBuffer信息、INODE信息、undo日志信息等等

索引页(数据页): 7部分:文件头部、页面头部、最大/最小记录(虚拟记录)、用户记录、空闲空间、页面目录、文件尾部; 一开始没有用户记录,插入数据时,从空闲空间分配地址到用户记录;

InnoDB维护一条记录的单链表,按照主键由小到大的顺序连接起来的。

next_record指针在头信息和真实数据之间;

页面目录:由槽组成,对页进行分组; 最小记录分组只能有一条数据、最大记录分组1到8条之间、剩下的在4到8之间; 槽代表的记录的主键有序,可以使用二分法快速找到槽,再根据槽代表的记录的next_record属性遍历;

页面头部:页中存储的记录的相关信息;

文件头部:针对不同页的通用信息;

文件尾部:检验页是否完整;

1.2.4 · 表空间

表空间tablespace -> 段segment -> 区extent -> 页page/块 -> 行row

独立表空间、系统表空间:

区 extent: 连续64个16KB的页,就是一个区,一个区默认占用1MB空间; 每256个区划分成一组; 每个组最开始的几个页面是固定的;

段 segment: 叶子节点有自己的区,非叶子节点也有自己的区; 分别的集合为两个段:叶子节点段、非叶子节点段; 也可能包含一些零散的页,见碎片区;

碎片区fragment:直接属于表空间,不是所有的页都属于一个段; 当开始往表里插数据时,先从某个碎片区以单个页为单位进行分配;当占用32个页后,以完整的区开始分配;

XDES Entry: 拓展描述实体,每一个区对应着一个,记录了对应区的一些属性(段ID,串联其他XDES的节点,状态,页是否空闲位图);

  1. 先找空闲碎片页
  2. 再找空闲区(取一些碎片页)
  3. 段中占满了32个碎片页,则直接申请完整区

INODE Entry:记录段中的属性

FSP_HDR类型页:表空间的第一个页:

1.3 · 索引

https://tech.meituan.com/2014/06/30/mysql-index.html

访问磁盘的代价,大约是访问内存十万倍左右 需要将磁盘IO次数控制在很小数量级 -> B+树 IO次数取决于树的高度

索引模型: 哈希链表、有序数组和N叉搜索树;InnoDB 使用 B+ 树索引模型;

1.3.1 · B+ 树索引#

数据页由双向链表组成;快速定位记录所在页;

索引:后一个页的最大主键,必须大于前一个页的最大主键;由目录项记录; 用户记录在B+树叶子结点,非叶子结点为目录项;

主键索引(聚簇索引):叶子节点存的是整行数据; 二级索引(只包含索引列和主键):非主键列的索引,只能查到主键,再回表查完整用户记录; 回表:先从二级索引拿key,再回主键索引取数据

索引在MYISAM:索引与数据分开存储;索引为主键+行号,根据行号拿数据;

1.3.2 · 索引使用

B+ 索引代价:

  1. 空间
  2. 增删改时,需要修改索引树

最左前缀原则(通过调整顺序,可以少维护一个索引) 遇到范围查询就停止; 字符串索引可以匹配前缀;后缀搜索时,逆序存储是一个办法;

用于排序,索引字段本身是已经排好序的;

需要回表的记录越多,二级索引的性能就越低(因为回表时访问聚簇索引是随机IO)

优化器决定:全表扫描,还是二级索引+回表

索引覆盖:查询时,最好只包含索引列:避免回表操作;覆盖索引、联合索引

索引下推:在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数

如何建立索引: 搜索、排序、分组的列创建索引; 基数大的列(不同值的个数); 小类型的列; 索引字符串的前缀(部分字符串); 主键自主递增:避免手动插入时,当前页空间不足,从而分裂两个页,造成性能损失; 尽量拓展索引而不是新建索引;避免重复索引;索引列不参与计算;

1.4 · 查询

1.4.1 · 单表查询

  1. 全表扫描
  2. 索引查询:主键等值、唯一二级索引等值、普通二级索引等值、索引列范围、扫描整个索引

访问方法: const:通过主键、唯一的二级索引列与常数的等值比较来定位一条记录,速度特别快,常数级别; ref:普通二级索引与常数等值比较;代价就是回表,匹配记录越多,则回表次数多,则代价大; ref_or_null:二级索引列等于常数或为NULL; range:索引列匹配某个范围;in 单点区间,大于小于连续区间; index:查询列和条件列都在索引里面;不用回表; all:全表扫描

In操作的效果,类似多个=用or连接起来;

索引合并:index merge Intersection合并(且): 使用多个二级索引查数据,使用查询到的id的交集进行回表查询; 避免回表操作的随机IO导致的性能问题; 特定情况:

  1. 二级索引列都是等值匹配;
  2. 主键列可以范围匹配; 有序ID回表取值:Rowid Ordered Retrieval

Union合并(或): 情况:

  1. 二级索引列都是等值匹配;
  2. 主键列可以范围匹配;

Sort-Union合并(数量较少情况): 先获取id,再排序,再Union合并

NOTE: 使用联合索引替代 Intersection索引合并;

1.4.2 · 多表连接

本质:多个表的数据,将匹配的数据进行组合(笛卡尔积)放入结果集;

执行过程:

  1. 确定第一个查询的表,称作驱动表;
  2. 查询第2张表,称作被驱动表;

内连接:驱动表在被驱动表找不到数据,则不加入结果集; 外连接:与内连接相反,加入结果集; 左外连接:选取左侧为驱动表 右外连接:选取右侧为驱动表

where子句,不符合要求,都不加入结果集; On子句,只在外连接有效

连接的原理: 嵌套循环连接:大于2张表时,前一次驱动表与被驱动表连接的结果,就是当前连接的驱动表 使用索引加快连接速度:多次查询被驱动表时,使用索引加快速度;const -> eq_ref 基于块的索引嵌套: 大数据量,需要多次进行IO,所以需要尽量减少IO; 多次访问被驱动表,多次IO:join buffer 默认256kb;

1.4.3 · 基于成本优化

成本:IO成本、CPU成本

成本常数:

  • 读取一个页面花费的成本默认是1.0
  • 检测一条记录是否符合搜索条件的成本默认是0.2

储存引擎层engine_cost表, 服务层server_cost表

单表:

  1. 根据搜索条件,找出所有可能使用的索引:possible keys
  2. 计算全表扫描的代价:页面数+记录数
  3. 计算使用不同索引执行查询的代价:回表数量
  4. 对比各种执行方案的代价,找出成本最低的那一个

通过访问索引B+树计算某个范围的索引数量,称作index dive

多表(连接): 连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数 x 单次访问被驱动表的成本 计算或猜(condition filtering)fanout数量; 多表:n表连接的顺序有n!种;

  1. 提前结束成本评估(每一种连接在计算时和一个全局最小成本变量比较)
  2. 表数小于optimizer_search_depth值,则穷举所有,否则只穷举到数量相等的表数
  3. 表顺序满足启发式规则才计算,否则忽略

1.4.4 · 收集统计数据

统计数据在磁盘、在内存;以表为单位收集;

在创建和修改表的时候通过指定STATS_PERSISTENT属性来指明该表的统计数据存储方式;

基于磁盘: 实际上存到这两张表innodb_index_stats, innodb_table_stats 定期更新innodb_stats_auto_recalc; 手动调用更新ANALYZE TABLE; 手动修改两张表;

索引列不重复的值的数量 innodb_stats_method:定义三种处理不同NULL方案(相等、不等、忽略)

1.4.5 · 基于规则优化

基于规则优化(查询重写):

简化搜索条件: 移除不必要的括号;常量传递;等值传递;移除无用条件;表达式计算; HAVING子句和WHERE子句的合并;常量表(主键查询或唯一二级索引查询);

外连接消除:指定的WHERE子句中包含被驱动表中的列不为NULL值的条件称之为空值拒绝reject-NULL),使得外连接和内连接没区别,从而可以优化连接顺序;

子查询优化: 返回的结果集:标量子查询/行子查询/列子查询/表子查询 与外层查询的关系:不相关/相关子查询

执行过程:

  • 不相关的标量/行子查询:MySQL会分别独立的执行外层查询和子查询;
  • 相关的标量/行子查询:先从外层查询中获取一条记录,再执行子查询;
  • 最后根据子查询的查询结果来检测外层查询WHERE子句的条件是否成立,成立则加入结果集,否则丢弃。

IN子查询优化: 物化表:结果写入临时表(基于内存(哈希索引)或磁盘(B+索引)); 物化表转内连接; semi-join:只关心匹配的记录是否存在,而不关心条数,因为只保存前张表的数据

派生表的优化:物化;重写为没有派生表的形式;

1.4.6 · EXPLAIN#

explain

select SQL_NO_CACHE * from TABLE;

type = ALL 全表扫描 type = range 范围扫描(通常使用索引)

1.4.7 · optimizer trace#

1.5 · 事务

ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)

事务状态:活动的/部分提交/失败/中止(回滚)/提交

begin / start transaction 命令不是事务的起点,执行随后的第一个操作表语句,事务才启动; 执行start transaction with consistent snapshot创建一致性视图;

roll back to save point.

1.5.1 · 隔离级别

https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read) 事务隔离级别:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)

1.5.2 · MVCC#

多版本并发控制 https://en.wikipedia.org/wiki/Multiversion_concurrency_control

如何实现:时间戳、增长的事务ID

Multi-Versioning: 保存row变更的多个版本数据;用于并发、回滚等功能; 每一row三个字段:DB_TRX_ID 上一进行插入或更新的事务, DB_ROLL_PTR 指向 undolog, DB_ROW_ID 当插入行时自增

每行数据也有版本,当事务提交时,将其事务ID赋值给这个数据的版本的事务ID row trx_id 一条行记录有多个版本,关联多个事务; 值不是物理上真实存在的,是通过当前版本和undo log计算出来的;

两个’视图’概念:

  • view: 使用查询语句定义的虚拟表;
  • consistent read view: InnoDB在实现MVCC时用到的一致性读视图,用于支持”读提交”、“可重复读”隔离级别实现;

事务更新的时候:当前读?如果当前行被其他事务占用,进入锁等待。

1.5.3 · 两阶段提交 2PC#

保证redolog和binlog这两种日志之间的一致性; prepare阶段:写redolog,标记prepare,写binlog; commit阶段:redolog标记commit,再决定是否binlog刷盘;

1.6 · 锁

全局锁、表级锁和行锁

1.6.1 · 全局锁

备份FTWRL,可重复读

1.6.2 · 表级锁

当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁;

意向锁(表级别,在表中的多个行上锁):包含意向共享锁、意向排他锁

1.6.3 · 行锁

在引擎层由各个引擎自己实现;在 InnoDB 事务中,行锁是在需要的时候才加上,事务结束时才释放;

两阶段锁 2PL:保证事务隔离性;多个事务在并发情况下等同于串行执行;锁被一个事务持有后,只会在提交或回滚后释放; 事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放,最大程度地减少了事务之间的锁等待;

死锁检测:超时、主动死锁检测然后回滚某一事务;

lock escalation 锁升级:行锁升级表锁

行级别锁类型:

  • 共享锁、排他锁:共享锁允许读行,排他锁允许更新、删除行;类似读写锁
  • 记录锁:索引记录;阻止其他事务插入、修改、删除对应记录;
  • 间隙锁:索引记录之间的锁;锁范围;
  • 邻键锁(Next-Key Lock):记录锁和间隙锁的结合;
  • 插入意向锁:在行插入之前由插入操作设置的间隙锁;
  • 自增锁;

一致性非锁定读:读快照 锁定读:共享锁、排他锁

1.7 · 日志

1.7.1 · redo log 重做日志#

InnoDB 引擎特有;Write-Ahead Logging, 先写日志(内存),再写磁盘; 循环写 ib_logfile0, ib_logfile1

记录事务对数据库的修改; 记录在某个页面的某个偏移量修改了几个字节的值,具体内容是啥;

结构:type, spaceID, pageNumber, data

redo log 的写入拆成了两个步骤:prepare 和 commit,是”两阶段提交”

1.7.2 · binlog 归档日志#

Server 层实现

1.7.3 · undo log#

https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-tablespaces.html https://dev.mysql.com/doc/refman/5.7/en/innodb-undo-logs.html http://mysql.taobao.org/monthly/2015/04/01 http://mysql.taobao.org/monthly/2023/05/01 https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system

保证原子性、隔离性、解决崩溃恢复问题 undo Log用来记录每次修改之前的历史值

存储在undo tablespace; 每个undo tablespace会划分128个rollback segment, 提升并发能力(写undo不发生冲突), 0~32用于临时表; rseg_array 每个rollback segment包含1024个undo slot(slot数据依据InnoDB的页面大小决定,默认16KB情况下1024个); TRX_RSEG_N_SLOTS 写事务执行时要分配一个rollback segment, 事务中任何一类写操作要分配一个undo slot, 该undo slot对应分配一个undo segment, 维护着一个undo页面链表,

Undo Segment -> Undo Page -> Undo Record

SHOW VARIABLES LIKE ‘%undo%’; SHOW VARIABLES LIKE ‘%innodb_rollback_segments%’; — value 128

删除,两阶段:

  1. 将记录的deleted_flag标示位设置为1
  2. 当该删除语句所在的事务提交之后,undo purge线程来真正的把记录删除掉

1.8 · 运维

1.8.1 · 读写分离

将数据库的读、写操作,分散到不同节点上 主机负责写;从机负责读

问题:主从复制、分配?

实现:代码实现、中间件

1.8.2 · 分库、分表 Sharding#

解决海量数据、大量请求问题

数据量大,查询慢:分表 高并发,查询多:分库

Sharding Key: 分库分表的依据 根据ID决定库,比如取余算法

水平分:按行分 垂直分:按列分

问题:join、事务

1.8.3 · 备份、恢复

1.8.4 · 物化视图

1.8.5 · 反规范化:存储冗余数据

1.9 · 源码

client —mysql协议—> server(sql解析、处理、优化…) —引擎接口—> engine

核心源码主要集中在: sql 和 storage/innobase

执行流: handle_connection -> do_command -> dispatch_command -> mysql_parse -> mysql_execute_command ->

storage/innobase/include/*0types.h 定义了数据结构

btr0types.h B+树 storage/innobase/btr/

trx0types.h 事务 storage/innobase/trx/

lock0types.h 锁 https://nicky-chin.cn/2022/03/17/mysql-lock-struct/

1.9.1 · B tree#

https://zhuanlan.zhihu.com/p/594678689

通过 btree 定位到具体物理页,再对物理页面内的record进行增删改查 在 innodb 通过 cursor 搜索实现

1.9.2 · 表

// https://github.com/mysql/mysql-server/blob/f7680e98b6bbe3500399fbad465d08a6b75d7a5c/storage/innobase/include/fil0fil.h#L144
struct fil_space_t {
};

// https://github.com/mysql/mysql-server/blob/f7680e98b6bbe3500399fbad465d08a6b75d7a5c/storage/innobase/include/dict0mem.h#L1330
struct dict_table_t {
};

// 创建表空间
// https://github.com/mysql/mysql-server/blob/f7680e98b6bbe3500399fbad465d08a6b75d7a5c/storage/innobase/include/dict0crea.h#L85
dict_build_tablespace_for_table()
// 分配段
// https://github.com/mysql/mysql-server/blob/f7680e98b6bbe3500399fbad465d08a6b75d7a5c/storage/innobase/include/fsp0fsp.h#L429
fseg_create_general()
// 分配区
// https://github.com/mysql/mysql-server/blob/f7680e98b6bbe3500399fbad465d08a6b75d7a5c/storage/innobase/fsp/fsp0fsp.cc#L1765
fsp_alloc_free_extent()
// 分配页
// 1. 从全局fsp分配
// https://github.com/mysql/mysql-server/blob/f7680e98b6bbe3500399fbad465d08a6b75d7a5c/storage/innobase/fsp/fsp0fsp.cc#L1929
fsp_alloc_free_page()
// 2. 从段上分配
// https://github.com/mysql/mysql-server/blob/f7680e98b6bbe3500399fbad465d08a6b75d7a5c/storage/innobase/fsp/fsp0fsp.cc#L3207
fseg_alloc_free_page_general()

// fsp_header_init() 表空间头
// fil_space_create() 表空间

// row 类型
// rec_format_enum
// mysql-server/storage/innobase/include/rem0types.h

创建表

// mysql_execute_command -> mysql_create_table -> mysql_create_table_no_lock -> create_table_impl -> rea_create_table -> ha_create_table -> handler::ha_create
// -> ha_innobase::create -> create_table_info_t::create_table -> create_table_info_t::create_table_def -> row_create_table_for_mysql -> ... -> dict_create_table_step
// -> dict_build_table_def_step -> dict_build_tablespace_for_table -> dict_build_tablespace_for_table

1.9.3 · 索引

mysql-server/storage/innobase/row/row0merge.cc

// mysql-server/storage/innobase/include/dict0mem.h:866
struct dict_index_t{

row_merge_create_index

1.9.4 · undo log#

// undo日志分为insert、update与delete这三类
// undo 类型: 更新删除都归为一类 update_undo
// mysql-server/storage/innobase/include/trx0rec.h
#define	TRX_UNDO_INSERT_REC	11	/* fresh insert into clustered index */
#define	TRX_UNDO_UPD_EXIST_REC	12	/* update of a non-delete-marked record */
#define	TRX_UNDO_DEL_MARK_REC	14	/* delete marking of a record; fields do not change */

// 回滚段
// mysql-server/storage/innobase/include/trx0rseg.h
struct trx_rseg_t {};

// undo log信息: trx_rseg_t 中多个链表
// mysql-server/storage/innobase/include/trx0undo.h
struct trx_undo_t {
	/* Fields for update undo logs */
	UT_LIST_BASE_NODE_T(trx_undo_t)	update_undo_list;
	UT_LIST_BASE_NODE_T(trx_undo_t)	update_undo_cached;
	/* Fields for insert undo logs */
	UT_LIST_BASE_NODE_T(trx_undo_t) insert_undo_list;
	UT_LIST_BASE_NODE_T(trx_undo_t) insert_undo_cached;
};

// 分配回滚段
// 1. 只读事务,临时表回滚段(第1~32号回滚段)
// trx_assign_rseg -->trx_assign_rseg_low --> get_next_noredo_rseg
// 2. 读写事务
// trx_set_rw_mode -->trx_assign_rseg_low --> get_next_redo_rseg

// 使用回滚段: trx_undo_report_row_operation -> trx_undo_assign_undo
// trx_undo_assign_undo 分配或创建: trx_undo_reuse_cached, trx_undo_create
// 加入到链表trx_rseg_t::insert_undo_list上

// 写入undo日志: trx_undo_report_row_operation
// 1. insert trx_undo_page_report_insert
// 2. 其他update trx_undo_page_report_modify

// 事务提交0 trx_prepare_low --> trx_undo_set_state_at_prepare
// 事务提交1 trx_commit_low --> trx_write_serialisation_history
// 事务回滚 row_undo_step --> row_undo

// 崩溃恢复
// mysql-server/storage/innobase/trx/trx0trx.cc
// trx_resurrect_insert()
// trx_resurrect_update()

// Undo页面结构
// mysql-server/storage/innobase/include/fil0fil.h
#define FIL_PAGE_UNDO_LOG	2	/*!< Undo log page */

// undo节点
// mysql-server/storage/innobase/include/row0undo.h
struct undo_node_t{};
// mysql-server/storage/innobase/row/row0undo.cc
row_undo_node_create()

// mysql-server/storage/innobase/row/row0uins.cc insert
row_undo_ins_parse_undo_rec()
// mysql-server/storage/innobase/row/row0umod.cc modify
row_undo_mod()