《Mysql是怎么运行的》读书笔记

95 阅读29分钟

1、innoDB的记录结构

COMPACT行格式 是紧凑的数据结构,目前Dynamic和Compressed行格式和COMPACT行格式比较类似,不同点是Dynamic会有溢出页地址,保存所有溢出数据,Compressed会将数据进行压缩

image.png

  1. 变长字段列表: 在 Compact 行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长 字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放,我们再次强调一遍,是逆序存放!
  2. NULL值列表: 我们知道表中的某些列可能存储 NULL 值,如果把这些 NULL 值都放到 记录的真实数据 中存储会很占地方,所 以 Compact 行格式把这些值为 NULL 的列统一管理起来,存储到 NULL 值列表中

3.记录头信息

image.png image.png 4.真实记录数据:

隐藏列: image.png 整体的存储结构:

image.png

2、innoBD的页结构

页的概念: 是 InnoDB 管理存储空间的基本单位,一个页的大小一般是 16KB 页结构:

image.png

image.png 存储的数据和页的关系:

image.png

image.png 每条数据的存储节点中的头信息:

delete_mask: 这个属性标记着当前记录是否被删除,占用1个二进制位,值为 0 的时候代表记录并没有被删除,为 1 的时 候代表记录被删除掉了(mysql并不是立刻删除数据,删除掉的数据会组成垃圾链表,如果后续有新的数据插入到表中,那么这些数据会被重复利用,这也因引申出空洞回收问题,删除表中数据,但是表的大小并不会立刻减少,需要进行空洞回收

n_owned:最小和最大记录的头信息中的 n_owned 属性

例如:最小记录的 n_owned 值为 1 ,这就代表着以最小记录结尾的这个分组中只有 1 条记录,也就是最小记录 本身。
最大记录的 n_owned 值为 5 ,这就代表着以最大记录结尾的这个分组中只有 5 条记录,包括最大记录本 身还有我们自己插入的 4 条记录。

min_rec_mask: B+树的每层非叶子节点中的最小记录都会添加该标记

heap_no:表示当前记录在本 页 中的位置,最小记录和最大记录是0,1;主要原因是好最快找到最小和最大值吧

record_type:有4种类型的记录, 0 表示普通记录, 1 表示B+树非叶节点记录, 2 表示最小记录, 3 表示最大记录 next_record:表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量,我们的记录按照主键从小到大的顺序形成了一个单链表,最大记录 的 next_record 的 值为 0 ,这也就是说最大记录是没有 下一条记录 了

页目录概念:

  1. 将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。
  2. 每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的 n_owned 属性表示该记录拥有多少条记 录,也就是该组内共有几条记录。
  3. 将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近 页 的尾部的地方,这个地方就是说的 Page Directory ,也就是 页目录 (此时应该返回头看看页面各个部分的图)。页面目录中的这些地址 偏移量被称为 槽 (英文名: Slot ),所以这个页面目录就是由 槽 组成的。
  4. 每个槽有4条数据

image.png 如何查找数据:

在一个数据页中查找指定主键值的记录的过程分为两步:

  1. 通过二分法确定该记录所在的槽,并找到该槽中主键值最小的那条记录。
  2. 通过记录的 next_record 属性遍历该槽所在的组中的各个记录。

Header属性

image.png

3、B+树索引

数据与页的整体关系:

image.png 页与页直接并不是顺序相连的,也就是说,页面1的链接的是页面20,页面20下一个是页面50等等

建立索引以后的页面 record_type为1的时候是目录记录,也就是索引列,索引页记录最小值,这个是在页面那节里面表示的min_rec_mask

image.png 聚簇索引:

  1. 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:

    页内的记录是按照主键的大小顺序排成一个单向链表。 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成 一个双向链表。

  2. B+ 树的叶子节点存储的是完整的用户记录。 所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

我们把具有这两种特性的 B+ 树称为 聚簇索引 ,所有完整的用户记录都存放在这个 聚簇索引 的叶子节点处。这 种 聚簇索引 并不需要我们在 MySQL 语句中显式的使用 INDEX 语句去创建(后边会介绍索引相关的语句),

InnoDB 存储引擎会自动的为我们创建聚簇索引。另外有趣的一点是,在 InnoDB 存储引擎中, 聚簇索引 就是数 据的存储方式(所有的用户记录都存储在了 叶子节点 ),也就是所谓的索引即数据,数据即索引。 二级索引: 需要回表查询的索引,比如一个表id是主建索引,同时也有一个name字段的索引,那么通过name查询的话,是需要查出用户的id,然后通过id去查数据,那么name就是二级索引 联合索引: 是多个字段的二级索引,通过最左匹配原则,从左向右以此排序,如果第一个字段相同,那么就按第二个字段排序,以此类推

MyISAM中的索引

表中的记录按照记录的插入顺序单独存储在一个文件中,称之为 数据文件 在插入数据的时候并没有刻意按照主键大小排序,所以我们并不能在这些数据上使用二分法进行查找 使用 MyISAM 存储引擎的表会把索引信息另外存储到一个称为 索引文件 的另一个文件中。 MyISAM 会单独为 表的主键创建一个索引,只不过在索引的叶子节点中存储的不是完整的用户记录,而是 主键值 + 行号 的组 合。也就是先通过索引找到对应的行号,再通过行号去找对应的记录!

image.png 索引的代价:

空间上的代价

需要建立存储空间

时间上的代价: 每次对表中的数据进行增、删、改操作时,都需要去修改各个 B+ 树索引(对比MyIsam区别)

索引的适用条件:

全值匹配: 即能全部匹配到所有索引 匹配左边的列: 如果我们想使用联合索引中尽可能多的列,搜索条件中的各个列必须是联合索引中 从最左边连续的列

匹配列前缀: SELECT * FROM person_info WHERE name LIKE '%As%'; 无法使用索引

SELECT * FROM person_info WHERE name LIKE 'As%'; 可以使用索引

匹配范围值:

精确匹配某一列并范围匹配另外一列: 用于排序:

排序也有使用到索引,所有使用order by的时候要注意,排序列的顺序也需要和索引列的顺序一致 用于分组:

group by 如果没 有索引的话,这个分组过程全部需要在内存里实现,而如果有了索引的话,恰巧这个分组顺序又和我们的 B+ 树 中的索引列的顺序是一致的,而我们的 B+ 树索引又是按照索引列排好序的,这不正好么,所以可以直接使用 B+ 树索引进行分组。 ,分组列的顺序也需要和索引列的顺序一致

回表的代价: 需要回表的记录越多,使用二级索引的性能就越低

覆盖索引: 为了彻底告别 回表 操作带来的性能损耗,我们建议:最好在查询列表里只包含索引列,很不鼓励用 * 号作为查询列 ,最好把我们需要查询的列依次标明。

在使用索引时需要注意下边这些事项:

  • 只为用于搜索、排序或分组的列创建索引
  • 为列的基数大的列创建索引
  • 索引列的类型尽量小
  • 可以只对字符串值的前缀建立索引
  • 只有索引列在比较表达式中单独出现才可以适用索引
  • 为了尽可能少的让 聚簇索引 发生页面分裂和记录移位的情况,建议让主键拥有 AUTO_INCREMENT 属性。 定位并删除表中的重复和冗余索引
  • 尽量使用 覆盖索引 进行查询,避免 回表 带来的性能损耗。

4、数据目录与表空间

区(extent)的概念:

对于16KB的页来说,连续的64个页就是一个区 ,也就是说一个区默认占用1MB空间大小。不论是系统 表空间还是独立表空间,都可以看成是由若干个区组成的,每256个区被划分成一组

  1. 页(Page)

    • 最小存储单元:默认大小 16KB(可配置)。
    • 存储实际数据(行记录)、索引、元数据等。
    • 所有 I/O 操作(读写)以页为单位。
  2. 区(Extent)

    • 连续页的集合:1 个区 = 64 个连续页(默认 1MB = 64 × 16KB)。
    • 空间分配的最小单位(表空间扩容时,一次至少分配 1 个区)。
  3. 段(Segment)

    • 逻辑数据集合:由多个区(Extent)组成。

    • 每个索引对应两个段:

      • 叶子节点段(Leaf Segment) :存储实际行数据(B+Tree 的叶子层)。
      • 非叶子节点段(Non-Leaf Segment) :存储索引节点(B+Tree 的非叶子层)。

二、设计目的与优势

1. 减少磁盘碎片

  • 问题:若每次分配单页,物理存储可能随机分散,导致随机 I/O 性能下降。
  • 解决:以区(Extent)为单位分配连续页,保证数据物理连续,减少寻道时间。

2. 提升顺序 I/O 效率

  • 范围查询(如 BETWEENORDER BY)需读取大量连续数据。
  • 区内的页物理连续,减少磁头移动,提高吞吐量。

3. 高效管理大对象

  • 大字段(如 BLOB)或长文本可能跨多个页。
  • 段(Segment)统一管理这些页,避免零散分配。

4. 平衡空间与性能

  • 小表:分配碎片页(Fragmented Pages) (不占整区),避免浪费空间。
  • 大表:按区分配,保证性能。
  • 段自动处理这种平衡,对开发者透明。

5、explain详解

   | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | r
    ows | filtered | Extra |

每个字段代表的含义: ID:每一张表都对应着执行计划输出中的一条记录,ID即那个记录

Select_type: image.png

6、事务与mvcc与锁

事务:

原子性

隔离性

一致性

持久性 事务隔离级别

脏写 如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了 脏写

image.png 脏读 一个事务读到了另一个未提交事务修改过的数据

image.png

不可重复读(Non-Repeatable Read) 一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值

image.png 幻读(Phantom) 一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先 的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了 幻读

image.png

4个 隔离级别:

READ UNCOMMITTED :未提交读。

READ COMMITTED :已提交读。

REPEATABLE READ :可重复读。

SERIALIZABLE :可串行化。

image.png

MVCC原理

版本链:

对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列( row_id 并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id 列):

  • trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的 事务id 赋值给 trx_id 隐藏列。
  • roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏 列就相当于一个指针,可以通过它来找到该记录修改前的信息。

ReadView

需要判断一下版本链中的哪个版本是当前事务可见的。为此,设计InnoDB的大叔提出了一个ReadView 的概念,这个 ReadView 中主要包含4个比较重要的内容:

  • m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的 事务id 列表。
  • min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id ,也就是 m_ids 中的最 小值。
  • max_trx_id :表示生成 ReadView时系统中应该分配给下一个事务的 id 值。
  • creator_trx_id :表示生成该 ReadView 的事务的 事务id 。

在读已提交和可重复读两个隔离级别下, 都是创建mvcc的视图(ReadView),不同的是创建的数量和时机不同,读已提交,每次都是创建一个视图,而可重复读只会在开始创建一个视图,所以对于读已提交,避免不了不可重复读的问题

是否可见:

  1. 如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己 修改过的记录,所以该版本可以被当前事务访问。
  2. 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生 成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  3. 如果被访问版本的 trx_id 属性值大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生 成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该 版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

MVCC小结

所谓的 MVCC (Multi-Version Concurrency Control ,多版本并发控制)指的就 是在使用 READ COMMITTD 、 REPEATABLE READ 这两种隔离级别的事务在执行普通的 SEELCT 操作时访问记录的版 本链的过程,这样子可以使不同事务的 读-写 、 写-读 操作并发执行,从而提升系统性能。 READ COMMITTD REPEATABLE READ 这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一 次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作 前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

锁:

锁结构 里有很多信息,不过为了简化理解,我们现在只把两个比较重要的属性拿了出来:

  • trx信息 :代表这个锁结构是哪个事务生成的。
  • is_waiting :代表当前事务是否在等待。

锁类型

  • 共享锁 ,英文名: Shared Locks ,简称 S锁 。在事务要读取一条记录时,需要先获取该记录的 S锁
  • 独占锁 ,也常称 排他锁 ,英文名: Exclusive Locks ,简称 X锁 。在事务要改动一条记录时,需要先获 取该记录的 X锁 。

对读加S锁:SELECT ... LOCK IN SHARE MODE

对读取的记录加X锁 : SELECT ... FOR UPDATE;

DELETE : 对一条记录做 DELETE 操作的过程其实是先在 B+ 树中定位到这条记录的位置,然后获取一下这条记录的 X 锁 ,然后再执行 delete mark 操作。我们也可以把这个定位待删除记录在 B+ 树中位置的过程看成是一个获取X锁的锁定读

UPDATE :

  1. 在对一条记录做 UPDATE 操作时分为三种情况: 如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化,则先在 B+ 树中定位 到这条记录的位置,然后再获取一下记录的 X锁 ,最后在原记录的位置进行修改操作。其实我们也可以 把这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 。
  2. 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在B+ 树中定位到这条记录的位置,然后获取一下记录的X锁 ,将该记录彻底删除掉(就是把记录彻底移 入垃圾链表),最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X 锁 的 锁定读 ,新插入的记录由 INSERT 操作提供的 隐式锁 进行保护。
  3. 如果修改了该记录的键值,则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作,加锁操作就 需要按照 DELETE 和 INSERT 的规则进行了。

INSERT :
一般情况下,新插入一条记录的操作并不加锁设计 InnoDB 的大叔通过一种称之为 隐式锁 的东东来保护这条新插入的记录在本事务提交前不被别的事务访问

表锁:

  • 意向共享锁,英文名: Intention Shared Lock ,简称 IS锁 。当事务准备在某条记录上加 S锁 时,需要先 在表级别加一个 IS锁 。

  • 意向独占锁,英文名: Intention Exclusive Lock ,简称 IX锁 。当事务准备在某条记录上加 X锁 时,需 要先在表级别加一个 IX锁 。

  • LOCK TABLES t READ : InnoDB 存储引擎会对表 t 加表级别的 S锁 。

  • LOCK TABLES t WRITE : InnoDB 存储引擎会对表 t 加表级别的 X锁 。

如何解决幻读的问题:

Gap Locks :

我们说 MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC 方 案解决,也可以采用 加锁 方案解决。但是在使用 加锁 方案解决时有个大问题,就是事务在第一次执行读取 操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上 正经记录锁

image.png

Next-Key Locks : 有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的 间隙 插入新记录,所以设计 InnoDB 的大 叔们就提出了一种称之为 Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,我们也可以简称为 next-key锁:next-key锁 的本质就是一个 正经记录锁 和一个 gap锁 的合体,它既能保护该条记录,又能阻止别的事务 将新记录插入被保护记录前边的 间隙 Insert Intention Locks : 有事务想在某个间隙 中插入新记录,但是现在在等待 隐式锁: 一个事务在执行 INSERT 操作时,如果即将插入的 间隙 已经被其他事务加了 gap锁 ,那么本次 INSERT 操作会阻塞,并且当前事务会在该间隙上加一个 插入意向锁 ,否则一般情况下 INSERT 操作是不加锁的

7、redo日志

redo是什么

  • redo日志会把事务在执行过程 中对数据库所做的所有修改都记录下来,在之后系统奔溃重启后可以把事务所做的任何修改都恢复出来
  • 在事务提交完成之前把该 事务所修改的所有页面都刷新到磁盘
  • 我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来 系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修 改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好

redo 日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容是啥 就好了 image.png 对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction ,简称 mtr: 一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo 日 志,

redo log block:

把通过 mtr 生成的 redo 日志都放在了大小为 512字节 的 页 中。为了和我们前边提到的表空间中的页做区别,我们这里把用来存储 redo 日志的页称为 block redo日志缓冲区 InnoDB 的大叔为了解决磁盘速度过慢的问题而引入了 Buffer Pool 。同理,写入 redo 日 志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的 连续内存空间,翻译成中文就是 redo日志缓冲区 ,我们也可以简称为 log buffer

redo日志刷盘时机:

  1. log buffer 空间不足时
  2. log buffer 的大小是有限的(通过系统变量 innodb_log_buffer_size 指定),如果不停的往这个有限大小 的 log buffer 里塞入日志,很快它就会被填满。设计 InnoDB 的大叔认为如果当前写入 log buffer
  3. redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
  4. 事务提交时 我们前边说过之所以使用 redo 日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改 过的 Buffer Pool 页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的 redo 日志刷新到 磁盘。
  5. 后台线程不停的刷刷刷 后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。
  6. 正常关闭服务器时
  7. 做所谓的 checkpoint 时(我们现在没介绍过 checkpoint 的概念,稍后会仔细唠叨,稍安勿躁)

Log Sequeue Number

redo 日志的量在不断的递增,就 像人的年龄一样,自打出生起就不断递增,永远不可能缩减了。设计 InnoDB 的大叔为记录已经写入的 redo 日志 量,设计了一个称之为 Log Sequeue Number 的全局变量,翻译过来就是: 日志序列号 ,简称 lsn 。不过不像 人一出生的年龄是 0 岁,设计 InnoDB 的大叔规定初始的 lsn 值为 8704 (也就是一条 redo 日志也没写入时,

lsn 的值为 8704 )

  • block剩余空闲空间能容纳这 个 mtr 提交的日志时, lsn 增长的量就是该 mtr 生成的 redo 日志占用的字节数
  • 待插入的block剩余空闲空间不足以容纳 这个 mtr 提交的日志时, lsn 增长的量就是该 mtr 生成的 redo 日志占用的字节数加上额外占用的 log block header 和 log block trailer 的字节数,就像这样

MySQL 的 Redo Log(重做日志) 是 InnoDB 存储引擎实现 崩溃恢复(Crash Recovery) 的核心机制,其底层设计围绕 顺序写入物理逻辑日志Write-Ahead Logging (WAL) 原则构建。以下是其底层实现原理的深入解析:


一、核心设计目标

  1. 持久性保证:确保已提交事务的数据修改不会因崩溃丢失。
  2. 高性能写入:将随机写页操作转换为顺序写日志操作。
  3. 崩溃恢复:重启时通过重放日志将数据恢复到一致状态。

二、关键组件与协作

1. Log Buffer(日志缓冲区)

  • 作用:事务提交前,Redo 日志先写入内存缓冲区(减少磁盘 I/O)。
  • 触发刷盘
    • 事务提交时(innodb_flush_log_at_trx_commit=1)。
    • Log Buffer 满(默认 16MB)。
    • 后台线程每秒刷新。

2. Redo Log Files(日志文件)

  • 物理文件:通常为 ib_logfile0ib_logfile1(循环写入)。
  • 固定大小:通过 innodb_log_file_sizeinnodb_log_files_in_group 配置。
  • 循环写入:文件写满后回溯到第一个文件覆盖写入(需结合 Checkpoint 机制)。

3. Log Sequence Number (LSN)

  • 全局递增:每个日志块(512 字节)分配唯一 LSN,标记日志写入位置。
  • 贯穿全流程
    • 数据页中记录最新修改的 LSN(Page LSN)。
    • Checkpoint 记录已刷盘数据的 LSN(Checkpoint LSN)。
    • 崩溃恢复时从 Checkpoint LSN 开始重放日志。

4. Write-Ahead Logging (WAL) 原则

graph LR
A[事务修改数据页] --> B[生成 Redo 日志]
B --> C[日志写入 Log Buffer]
C --> D[日志刷盘到 ib_logfile]
D --> E[数据页异步刷盘]
  • 严格顺序Redo 日志必须先于数据页持久化到磁盘
  • 崩溃恢复依据:即使数据页未刷盘,只要 Redo 日志完整,就能恢复数据。

三、Redo 日志的物理结构

1. 日志块(Log Block)

  • 固定大小 512 字节:与磁盘扇区对齐,保证写入原子性。
  • 头部信息(12 字节):
    • LOG_BLOCK_HDR_NO:块序号
    • LOG_BLOCK_HDR_DATA_LEN:块内有效日志长度
    • LOG_BLOCK_FIRST_REC_GROUP:第一个日志记录的偏移量
  • 尾部校验和(4 字节):LOG_BLOCK_TRL_NO

2. 日志记录(Log Record)

  • 物理逻辑格式
    • 物理:记录修改的表空间 ID + 数据页号
    • 逻辑:记录页内具体修改操作(如 B+树分裂、行记录更新)。
  • 示例结构
    | 表空间ID (4B) | 页号 (4B) | 页内偏移 (2B) | 修改内容 (N 字节) |
    

四、崩溃恢复流程

sequenceDiagram
    participant Crash as 崩溃发生
    participant Startup as MySQL 重启
    participant System as InnoDB 存储引擎
    
    Crash ->> Startup: 数据库异常终止
    Startup ->> System: 启动崩溃恢复
    System ->> System: 定位 Checkpoint LSN
    System ->> System: 从 Checkpoint LSN 扫描 Redo Log
    System ->> System: 重放日志(应用修改到数据页)
    System ->> System: 回滚未提交事务(Undo Log)
    System ->> Startup: 恢复完成,数据库可用
  1. 定位起点:读取最新的 Checkpoint LSN(存储在日志文件头)。
  2. 扫描日志:从 Checkpoint LSN 开始顺序读取 Redo 日志。
  3. 重放修改
    • 对比日志中的 Page LSN 和数据页当前的 Page LSN
    • 若日志 LSN > 数据页 LSN,说明修改未刷盘,重放该日志。
  4. 回滚未提交事务:利用 Undo Log 回滚未提交事务(保证原子性)。

五、高性能关键优化

1. 组提交(Group Commit)

  • 合并刷盘:多个事务的日志合并为一次 I/O 写入磁盘。
  • 三阶段提交
    graph LR
    A[Flush Stage] --> B[Sync Stage]
    B --> C[Commit Stage]
    
    • Flush:将日志写入 Log Buffer。
    • Sync:多个事务的日志一次性刷盘。
    • Commit:事务标记为已提交。

2. 日志缓冲区(Log Buffer)

  • 减少磁盘 I/O:累计多个事务日志后批量写入。
  • 异步刷盘:由后台线程定期刷新。

3. Checkpoint 机制

  • 推进 Checkpoint LSN:当数据页刷盘后,更新 Checkpoint 位置。
  • 释放日志空间:允许覆盖旧日志(LSN < Checkpoint LSN)。

六、Redo Log 与 Binlog 的协作

在事务提交时,通过 两阶段提交(2PC) 保证 Redo Log 和 Binlog 的一致性:

  1. Prepare 阶段:写入 Redo Log(标记为 PREPARE)。
  2. Write & Sync Binlog:生成 Binlog 并刷盘。
  3. Commit 阶段:Redo Log 标记为 COMMIT

⚠️ 崩溃恢复逻辑

  • 若发现 PREPARE 的 Redo Log 但无对应 Binlog → 回滚事务。
  • 若 Binlog 完整存在 → 提交事务(重放 Redo)。

总结:Redo Log 的核心价值

  1. 顺序写替代随机写:将数据页的随机写转为日志的顺序写,大幅提升写入性能。
  2. 崩溃恢复基石:通过 LSN + Checkpoint + WAL 实现秒级崩溃恢复。
  3. 事务持久化保证:确保已提交事务的数据永不丢失(ACID 中的 D)。

📌 关键配置建议

  • innodb_flush_log_at_trx_commit=1:每次提交刷盘(保证持久性)。
  • innodb_log_file_size=4G:增大日志文件减少 checkpoint 频率。
  • innodb_log_buffer_size=64M:避免高频事务导致 Log Buffer 溢出。

8、undo日志

Undo Log(回滚日志) 是 InnoDB 存储引擎实现事务 原子性(Atomicity)多版本并发控制(MVCC) 的核心机制。它的核心作用是记录数据修改前的状态,用于事务回滚或提供一致性读视图。以下是其底层原理的深度解析:


一、核心作用

  1. 事务回滚(Rollback)
    记录数据修改前的值,支持事务失败时回滚到修改前的状态。
  2. MVCC(多版本并发控制)
    为其他事务提供历史版本数据,实现非阻塞读(快照读)。
  3. 崩溃恢复一致性
    与 Redo Log 协作,保证崩溃后未提交事务的正确回滚。

二、底层实现机制

1. Undo Log 存储结构

  • 存储位置:位于系统表空间的 回滚段(Rollback Segments) 中(默认 128 个回滚段,可通过 innodb_rollback_segments 配置)。
  • 逻辑结构
    • 每个回滚段包含 1024 个 Undo Slot(每个 Slot 对应一个事务的 Undo Log)。
    • 每个 Undo Log 由多个 Undo Record 组成(链式存储)。
  • 物理存储
    Undo Log 以 段(Segment) 形式管理(类似数据段),存储在共享表空间(ibdata1)或独立 Undo 表空间(MySQL 8.0+ 默认启用)。

2. Undo Record 结构

每条 Undo Record 包含以下关键信息:

字段说明
TRX_ID生成该记录的事务 ID
ROLL_PTR指向更早版本数据的指针(构建版本链)
UNDO_TYPE操作类型(INSERT/UPDATE/DELETE
TABLE_ID表空间 ID
主键信息被修改行的主键值(用于定位数据)
旧数据镜像修改前的列值(回滚依据)

📌 示例
事务 TRX100name="Alice" 更新为 name="Bob" 时,Undo Record 会记录:

  • TRX_ID=100
  • 旧值 name="Alice"
  • 该行主键值(如 id=5

3. 版本链构建

每行记录隐藏两个字段:

  • DB_TRX_ID:最后修改该行的事务 ID。
  • DB_ROLL_PTR:指向该行对应的 Undo Record 指针。
graph LR
  A[当前行 id=5<br>name='Bob'<br>DB_TRX_ID=100<br>DB_ROLL_PTR=0x123] --> B[Undo Record 0x123<br>旧值 name='Alice'<br>TRX_ID=50<br>ROLL_PTR=0x456]
  B --> C[Undo Record 0x456<br>旧值 name='Tom'<br>TRX_ID=30<br>ROLL_PTR=NULL]

事务通过 ROLL_PTR 回溯找到历史版本数据。


三、工作流程剖析

场景:事务更新一行数据

  1. 生成 Undo Record
    • 将旧值 name="Alice" 写入 Undo Log。
    • 记录事务 ID(TRX_ID=100)及主键信息。
  2. 修改数据行
    • 更新数据页中的 name="Bob"
    • 设置 DB_TRX_ID=100(当前事务ID)。
    • 设置 DB_ROLL_PTR 指向新生成的 Undo Record。
  3. 事务提交/回滚
    • 提交:Undo Log 不会立即删除(需支撑 MVCC)。
    • 回滚:根据 Undo Record 恢复旧值 name="Alice"

四、与 MVCC 的协作

  1. 快照读(Snapshot Read)
    事务首次读时生成 Read View(包含活跃事务ID列表),通过版本链找到可见的旧版本数据。
    -- 事务A(TRX_ID=200)的 Read View 包含活跃事务 [100, 150]
    SELECT * FROM users WHERE id=5; 
    -- 忽略 TRX_ID=100(活跃中)的修改,读取 TRX_ID=50 的旧版本(name='Alice')
    
  2. 避免脏读
    只读取已提交事务或自身事务的修改。

五、Undo Log 的生命周期管理

1. 提交事务后的处理

  • 短期保留:为 MVCC 和回滚服务保留 Undo Log。
  • Purge 线程清理
    后台线程扫描并清理无用的 Undo Log(对应事务已提交且无快照依赖)。

2. 独立 Undo 表空间(MySQL 8.0+)

  • 默认创建 2 个独立 Undo 表空间(undo_001undo_002)。
  • 优势
    • 避免系统表空间膨胀。
    • 支持动态调整大小(ALTER UNDO TABLESPACE)。
    • 支持 TRUNCATE 快速回收空间。

六、崩溃恢复中的关键角色

  1. Redo 保护 Undo
    Undo Log 的写入也通过 Redo Log 保护(避免 Undo 本身丢失)。
  2. 恢复流程
    • 重放 Redo Log 恢复数据页 和 Undo 段
    • 扫描 Undo Log 回滚所有 未提交事务(状态为 PREPARE 或无对应 Binlog)。

七、性能优化关键点

  1. 长事务问题
    长时间未提交的事务会阻止 Purge 线程清理旧 Undo Log,导致表空间膨胀。
    • 监控:检查 information_schema.INNODB_TRX 中的长事务。
    • 解决:设置 innodb_max_undo_log_size + 拆分大事务。
  2. Undo 表空间监控
    -- 查看 Undo 空间使用
    SELECT TABLESPACE_NAME, FILE_SIZE, ALLOCATED_SIZE 
    FROM INFORMATION_SCHEMA.INNODB_TABLESPACES 
    WHERE TABLESPACE_NAME LIKE 'undo%';
    

总结:Undo Log 的核心价值

功能实现机制
事务原子性记录旧值,支持回滚操作
MVCC 多版本读构建数据版本链
崩溃恢复一致性与 Redo 协作回滚未提交事务
非阻塞读写写操作不阻塞快照读

💡 关键理解

  • Undo Log 是 逻辑日志(记录数据变化),而 Redo Log 是 物理日志(记录页修改)。
  • 二者共同保障了 InnoDB 的 ACID 特性,是事务系统的基石。