MySQL三大日志
Undo Log(回滚日志)
作用
事务回滚时恢复数据到修改前的状态。
支持 MVCC,为读操作提供历史版本数据。
存储
存放在 undo tablespace 中,通过回滚段管理。
格式
undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
通过 trx_id 可以知道该记录是被哪个事务修改的;
通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;
Redo Log(重做日志)
作用
保证事务的持久性,记录物理页的修改(物理日志,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志),用于崩溃恢复。
写入时机
事务执行中按顺序循环写入,通过 innodb_flush_log_at_trx_commit 控制刷盘策略(0/1/2)。
组成
内存中的 redo log buffer 和磁盘上的 redo log file。
undo log 和 redo log 区别
undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;
redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;
事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务
Binlog(二进制日志)
MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。
作用
记录所有对数据库的修改操作(逻辑日志),用于主从复制、数据恢复(如通过 mysqlbinlog 工具恢复数据)。
写入时机
事务提交后顺序写入,通过参数 sync_binlog 控制刷盘策略。
格式
STATEMENT(记录SQL语句)、ROW(记录行数据变化)、MIXED(混合模式)。
redo log 和 binlog 区别
1、适用对象不同:
binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
redo log 是 Innodb 存储引擎实现的日志;
2、文件格式不同:
binlog 记录每一条修改数据的 SQL (相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),
redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
3、写入方式不同:
binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
4、用途不同:
binlog 用于备份恢复、主从复制;
redo log 用于掉电等故障恢复。
MySQL Buffer Pool
核心概念
MySQL 的数据都是存在磁盘中的,那么我们要更新一条记录的时候,得先要从磁盘读取该记录,然后在内存中修改这条记录。修改完这条记录不是直接写回到磁盘,而是缓存起来。
为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
Buffer Pool 是 InnoDB 引擎的关键内存区域,用于缓存数据页和索引页,减少磁盘 I/O,提升数据库性能。
所有读写操作首先作用于 Buffer Pool,修改后的页称为脏页(Dirty Page),最终由后台线程刷回磁盘。
当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
Buffer Pool 配置
大小设置:
通过参数 innodb_buffer_pool_size 配置,通常设为系统内存的 50%-70%。
多实例:
在高并发场景下,可通过 innodb_buffer_pool_instances 分割为多个实例,减少锁竞争。
内存管理与 LRU 算法
LRU 链表优化:
InnoDB 将 LRU 链表分为 Young 区(热点数据)和 Old 区(新加载数据),避免全表扫描冲刷热点数据。
Old SubList:新数据页首先插入 Old 区头部。
Young SubList:若 Old 区的页在 innodb_old_blocks_time(默认 1 秒)后被再次访问,则移到 Young 区。
预读机制:
线性预读(Linear Read-ahead):基于顺序访问模式,预读相邻页。
随机预读(Random Read-ahead):根据页的访问模式预测加载其他页(默认关闭)。
脏页刷新与持久化
刷新机制:
Checkpoint:定期将脏页刷盘,崩溃恢复时从 Checkpoint 位置开始重放 redo log。
自适应刷新(Adaptive Flushing):根据负载动态调整刷盘速率,避免 I/O 尖峰。
刷盘触发条件:
Buffer Pool 空间不足时。
Redo Log 空间不足(通过日志推进检查点)。
后台线程定期刷新。
与日志系统的协作
Redo Log:事务提交时先写 redo log,确保即使脏页未刷盘,崩溃后仍可通过 redo log 恢复。
Undo Log:用于事务回滚和 MVCC,其历史版本数据可能存储在 Buffer Pool 的 undo 页中。
事务隔离级别
MySQL 支持四种隔离级别,通过 MVCC 和锁机制实现:
READ UNCOMMITTED(读未提交)
事务可以读到其他未提交事务的修改。
问题:脏读、不可重复读、幻读。
READ COMMITTED(读已提交,RC)
事务只能读到其他已提交事务的修改。
实现:每次 SELECT 生成新的 ReadView。
问题:不可重复读、幻读。
REPEATABLE READ(可重复读,RR)
默认隔离级别,事务内多次读取同一数据的结果一致。
实现:事务第一次 SELECT 时生成 ReadView,后续复用。
解决:脏读、不可重复读;通过间隙锁(Gap Lock) 避免幻读(当前读时生效)。
SERIALIZABLE(串行化)
所有操作加锁,强制事务串行执行。
解决:所有并发问题,但性能最低。
MVCC
MVCC(多版本并发控制 Multi-Version Concurrency Control)是数据库实现高并发访问的核心机制,通过维护数据的多个版本,允许读操作不阻塞写操作,写操作也不阻塞读操作。InnoDB 引擎使用 MVCC 实现了 非阻塞读,显著提升了数据库的并发性能,尤其在高读低写的场景下效果显著。
MVCC 的核心原理

隐藏字段
InnoDB 的每行数据包含两个隐藏字段:
DB_TRX_ID:最近修改该行数据的事务 ID(事务提交时写入)。
DB_ROLL_PTR:指向 Undo Log 的回滚指针,用于构建数据的历史版本链。
Undo Log
存储数据的历史版本,通过回滚指针(DB_ROLL_PTR)串联成一个链表(版本链)。
作用:
事务回滚时恢复数据到旧版本。
提供 MVCC 所需的历史版本数据。
ReadView(读视图)
事务在发起读操作时生成的一个快照,用于判断数据版本的可见性。
关键信息:
m_ids:生成 ReadView 时活跃(未提交)的事务 ID 集合。
min_trx_id:活跃事务中的最小事务 ID。
max_trx_id:生成 ReadView 时系统将分配给下一个事务的 ID。
creator_trx_id:创建该 ReadView 的事务 ID(仅当该事务自身有修改时存在)。
数据可见性规则
MVCC 通过以下规则判断数据版本是否对当前事务可见:
如果数据版本的 DB_TRX_ID < min_trx_id,说明该版本在 ReadView 生成前已提交,可见。
如果数据版本的 DB_TRX_ID >= max_trx_id,说明该版本由未来事务修改,不可见。
如果 DB_TRX_ID 在 m_ids 中,说明该版本由未提交的事务修改,不可见。
如果 DB_TRX_ID 不在 m_ids 中且 DB_TRX_ID < max_trx_id,说明该版本已提交,可见。
特殊处理:
如果该版本由当前事务自身修改(DB_TRX_ID == creator_trx_id),则可见。
MVCC 与隔离级别
不同隔离级别下,MVCC 的行为差异主要体现在 ReadView 的生成时机:
READ COMMITTED(读已提交,RC)
每次 SELECT 都生成新的 ReadView。
效果:能读到其他事务已提交的最新数据,但可能导致不可重复读。
REPEATABLE READ(可重复读,RR)
事务第一次 SELECT 时生成 ReadView,后续复用。(每开启一个事物才会生成一个 readView,一个事务的所有SQL语句共享一个 readView。)
效果:整个事务中看到的数据版本一致,解决不可重复读。
MVCC 如何解决并发问题
脏读
不可见未提交事务的版本(通过 ReadView 过滤活跃事务的修改)。
例:
如下所示是 student 学生表中的一条数据
(1) 假设现在有事务A和事务B两个事务并发操作该行数据。事务A执行了两次查询操作,事务B执行了一次更新操作;
(2)事务A执行第一次查询操作,先生成 readView ,我们姑且称之为 readView_1,还未开始查询操作,事务B率先执行了更新操作,将数据进行了修改并提交,事务B结束,此时事务A第一次查询开始,但由于事务A已经生成了 readView_1 ,所以它不会读取到事务B修改过后的数据,读取到的是 readView_1 中事务B修改之前的数据,解决了脏读的问题;
(3)然后,事务A进行第二次查询操作。注意!!!这里它又生成了一个 readView,我们称之为 readView_2,此时的 readView_2 中的数据是已经被事务B修改后的数据了,事务A再次进行查询,发现查询到的数据和刚操第一次查询到的不一样了,就产生了不可重复读的问题。
(4)所以说,MVCC 在读已提交隔离级别下只解决了脏读的问题,没有解决不可重复读的问题。
不可重复读
RR 隔离级别下,复用同一个 ReadView 保证多次读取结果一致。
例:
仍以上面的学生表举例,如下所示
(1)假设现在事务A与事务B并发操作来查询 student 表。事务A 执行SELECT查询操作,执行查询操作之前会生成一个 readView,我们姑且称之为 readView_1 ,事务A从始至终使用的都是 readView_1;
(2)此时事务B来修改 student 数据,可重复读隔离级别中一个事务生成一个 readView ,所以事务B也生成了一个 readView ,我们称之为 readView_2,然后事务B率先修改完毕并提交;
(3)事务A在事务B提交之后才进行的查询,按道理来说因为事务B修改了数据,我们会产生不可重复读,但是因为事务A从始至终都是用的 readView_1 ,所以 事务A在进行查询操作的时候,查询到的其实还是事务B修改之前的数据,由此就解决了不可重复读;而且即便事务A后续进行了多次 SELECT 查询操作,仍然使用最开始生成的 readView,解决了 不可重复读的问题。
(4)总结:其实归根结底,读已提交,可重复读两种隔离级别最关键的因素就是 readView 生成的时机不同,造就了它们不同的隔离级别。
幻读
幻读(Phantom Read)是数据库事务隔离性问题的一种,表现为:同一事务中多次执行相同范围的查询,后续查询返回了其他事务插入的新行。MySQL 在 可重复读(RR) 隔离级别下,通过 MVCC 和 间隙锁(Gap Lock) 结合的方式解决幻读问题。以下是详细分析:
幻读的本质
场景示例:
事务 A 查询 age > 20 的用户,返回 10 条数据。
事务 B 插入一条 age = 25 的新用户并提交。
事务 A 再次查询 age > 20,发现返回了 11 条数据(多出事务 B 插入的行)。
核心问题:范围查询中新增的数据破坏了事务的一致性视图。
MySQL 解决幻读的机制
MVCC(快照读)
原理:
在 RR 隔离级别下,事务首次读取数据时生成 ReadView,后续查询复用该快照,确保多次读取同一范围的数据时,结果一致(基于历史版本)。
效果:
快照读(普通 SELECT):通过 MVCC 读取历史版本数据,不会看到其他事务插入的新行,避免了幻读。
局限性:若事务中执行写操作(如 UPDATE/DELETE),可能触发当前读,导致幻读风险。
间隙锁(Gap Lock)
原理:
对索引记录之间的"间隙"加锁,阻止其他事务在范围内插入新数据。
间隙范围:例如,表中现有 id=5 和 id=10 的记录,间隙锁会锁定 (5, 10) 区间。
加锁场景:
执行 SELECT ... FOR UPDATE(当前读)。
执行 UPDATE 或 DELETE 影响范围内的数据。
效果:
其他事务无法在加锁的间隙内插入新数据,彻底避免幻读。
仅在 RR 隔离级别下生效。
不同隔离级别下的幻读行为

RR 隔离级别下的幻读解决方案
快照读 + MVCC(隐式解决)
场景:事务中仅执行普通 SELECT 查询(快照读)。
示例:
java
-- 事务 A(RR 隔离级别)
START TRANSACTION;
SELECT * FROM users WHERE age > 20; -- 生成 ReadView,返回 10 条数据
-- 事务 B 插入新数据并提交
INSERT INTO users (age) VALUES (25);
COMMIT;
-- 事务 A 再次查询
SELECT * FROM users WHERE age > 20; -- 仍返回 10 条数据(ReadView 未更新)
结果:事务 A 的两次查询结果一致,MVCC 屏蔽了事务 B 的插入操作。
当前读 + 间隙锁(显式解决)
场景:事务中执行 SELECT ... FOR UPDATE、UPDATE 或 DELETE(当前读)。
示例:
java
-- 事务 A(RR 隔离级别)
START TRANSACTION;
SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 当前读,对 age > 20 的间隙加锁
-- 事务 B 尝试插入新数据(age=25)
INSERT INTO users (age) VALUES (25); -- 被阻塞,直到事务 A 提交或回滚
-- 事务 A 提交后,事务 B 才能继续执行
COMMIT;
结果:事务 B 的插入操作被间隙锁阻塞,事务 A 的两次查询结果一致。
MVCC 的实现细节
版本链遍历
从当前数据行的 DB_ROLL_PTR 开始,沿着 Undo Log 链表查找符合可见性规则的版本。
Undo Log 清理
当没有事务需要访问某个历史版本时,对应的 Undo Log 会被 Purge 线程清理。
风险:长事务可能导致 Undo Log 堆积,引发存储膨胀。
事务 ID 分配
事务 ID(DB_TRX_ID)是全局自增的,每个事务在修改数据时被分配唯一 ID。
快照读 vs 当前读
快照读(Snapshot Read)
操作:普通 SELECT 语句(不加锁)。
实现:基于 MVCC 读取历史版本数据。
特点:不阻塞写操作,但可能读到旧数据。
当前读(Current Read)
操作:SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT。
实现:读取最新数据并加锁(如行锁、间隙锁),确保操作的一致性。
特点:阻塞其他事务的并发修改,保证数据最新。