大家好,我是程序员强子。
昨天学习了Mysql 四大金刚之一的 索引 ,今天则专注把另一个金刚 日志 相关给弄明白~
- redo log /undo log / binlog 核心作用,特点,工作流程,案例
- 两阶段提交 /double write /write-aheard-logging (WAL) 到底是什么?有什么作用?使用它们目的是什么?
干货很多,来不及解释,赶紧上车~
redo log
是ACID 中的 D ,持久性
文件格式 & 内容
格式
ib_logfile + 数字序号 ,默认生成 2 个文件 ib_logfile0 和 ib_logfile1;
固定大小的文件,追加写入到文件末尾,会循环覆盖旧日志
内容
记录的是 数据页的物理修改 ,循环写
比如:表 t 的数据页 10 中,偏移量 50 的值从 10 改成 20
而非 SQL 逻辑
因此恢复时无需解析 SQL,直接应用数据页修改,速度极快
刷盘策略
核心控制参数:innodb_flush_log_at_trx_commit
取值有 0、1、2 三种,默认值为 1(强可靠性优先)
-
0
- 事务提交时不刷盘,由 InnoDB 后台线程每秒刷盘,可能丢失 1 秒内的已提交数据
- 非核心业务(如日志存储、测试环境)
-
1(默认,最安全):
- 事务提交时,强制刷盘(fsync),确保 redo log 持久化;
- 金融、支付等核心业务(需严格保证 ACID)
-
2:
- 事务提交时,写入操作系统缓存(page cache),由操作系统每秒刷盘;
- 大部分非核心业务(如电商普通订单、用户行为数据)
核心作用
两大核心作用: 保证事务持久性 + 提升写入性能
如何保证事务持久性?
事务提交后,redo log 会先记录 数据页要做什么修改
再异步刷新数据文件(.ibd)
崩溃后可通过 redo log 恢复未刷盘的修改
如何提升写入性能?
数据修改时,若直接刷写数据文件(.ibd),会产生大量随机IO(速度慢);
为何 会产生 随机IO呢? 因数据页在磁盘上分散存储,查找也费力~
而 redo log 是 顺序IO
只需先将修改记录到 redo log
数据页(脏页)可后续由后台线程批量刷新,极大提升写入吞吐量
顺序IO 和随机IO 有什么区别?
顺序IO 顾名思义,连续地址读写 ,无跳转
而随机IO 离散地址读写,频繁跳转
- 机械硬盘 相差 10
100 倍,顺序读写≈100MB/s+,随机 IO 常 < 110MB/s - 固态硬盘 相差 2
10 倍,顺序≈500MB/s1GB/s,随机≈50~200MB/s
正常事务执行流程
假设执行 SQL:update t set a=20 where id=1;
正常流程:
-
读取数据到内存
- InnoDB 从磁盘读取 id=1 对应的 data page(数据页)到 Buffer Pool
-
修改内存数据页
- 在 Buffer Pool 中修改数据页的 a 值(从 10→20),此时数据页成为脏页
-
记录 redo log 到内存缓冲区
- 生成一条 redo log 记录
- 包含:数据页地址、偏移量、修改前值、修改后值、事务 ID 等)
- 写入redo log buffer
-
刷redo log到磁盘
- 根据刷盘策略(由 innodb_flush_log_at_trx_commit 控制)
- 将 redo log buffer 中的内容刷到磁盘的 redo log 文件
-
事务提交成功
- redo log 刷盘完成后,事务返回 提交成功,不关注脏页是否刷盘
-
后台异步刷脏页
- InnoDB 的 master thread 等后台线程,会在空闲时(如 Buffer Pool 满、系统负载低)
- double write机制 保证 InnoDB 数据页的完整性
- 将 Buffer Pool 中的脏页批量刷新到数据文件(.ibd)
什么是double write机制?跟着强子仔细研究一下:
double write机制
触发时机
属于 Buffer Pool 中的脏页批量刷新到数据文件(.ibd)
触发场景
- 后台线程主动刷脏:InnoDB 的 master thread 等后台线程,在空闲时(如系统负载低)批量刷新 Buffer Pool 中的脏页;
- Buffer Pool 满时刷脏:新数据页需加载到 Buffer Pool,而空间不足时,会淘汰旧脏页并触发刷盘;
- 手动触发刷脏:执行 flush tables、alter table 等语句,或设置 innodb_max_dirty_pages_pct 阈值触发;
- Checkpoint 触发:InnoDB 的 Checkpoint 机制(如 Sharp Checkpoint、Fuzzy Checkpoint)触发脏页批量刷盘
工作流程
-
内存数据页修改:事务执行时,Buffer Pool 中的数据页被修改为脏页;
-
写入 double write buffer:脏页刷盘前,先将完整的数据页写入内存中的double write buffer(全局共享缓冲区);
-
刷盘到 double write 文件:将 double write buffer 中的数据页批量刷盘到磁盘上的「double write 文件」(ibdata1 中或独立表空间),此过程为顺序写,性能高效;
-
刷盘到数据文件:确认 double write 文件写入成功后,再将数据页刷盘到实际的 .ibd 数据文件(随机写,因数据页分散存储);
-
崩溃恢复流程:数据页部分写入,异常中断,重启时 InnoDB 会:
- 检测到 .ibd 中的损坏数据页;
- 从 double write 文件中读取该页的完整备份,覆盖损坏页;
- 通过 redo log 对该完整页应用未完成的修改,最终恢复数据一致性。
崩溃恢复流程
触发时机:在 事务提交成功 后 ,后台异步刷脏页 前,发生故障崩溃(脏页未刷盘)
重启时触发恢复:
- InnoDB 启动时,扫描 redo log 文件组;
- 过滤出 已提交事务的 redo log 记录,通过事务ID 区分,未提交事务的记录忽略
- 将这些记录对应的修改,重新应用到对应的数据页
- 应用完成后,删除已失效的 redo log 记录(已刷盘的记录) ,恢复完成
特点
-
是InnoDB 独有的事务日志 ,二进制格式 的日志,非明文 SQL 或文本。
-
采用预写日志(WAL 机制)方式
-
崩溃安全(Crash-Safe)
- 无论正常关闭还是异常崩溃,InnoDB 重启时都会扫描 redo log
- 将 已记录但未刷到数据文件 的修改重新应用到数据页,保证数据一致性
-
与事务绑定
- 每个 redo log 记录都包含事务 ID(XID)
- 已提交事务的修改会最终保留,未提交事务的 会在崩溃恢复时会被忽略
上文提到WAL机制 ,什么是WAL预写日志呢?
不用急,跟着强子的脚步继续探寻~
WAL
特点
- 修改数据前,必须先将 数据修改记录 写入日志
- 日志刷盘持久化后,再修改内存中的数据页;
- 后续数据页会异步、批量刷到磁盘(.ibd 文件)
简单来说: 日志先行,数据后写,日志是数据修改的 前置保障
作用
- 保证crash-safe : 崩溃不丢数据
- 极大提升写入性能 : 顺序 IO VS 随机IO
- 减少刷盘开销: 不用每次修改都立即刷数据页到磁盘, 并且是批量,减少IO次数
binlog
由 MySQL 服务器(Server 层)生成,而非存储引擎,因此支持所有存储引擎
文件格式 & 内容
格式
- 以文件组形式存储(默认 mysql-bin.000001、mysql-bin.000002...)
- 按配置自动轮转,不会覆盖旧日志,需手动清理或设置过期时间
内容
记录的是数据修改的逻辑操作,而非物理地址修改
刚好 redo log 物理日志相反
仅记录已提交事务的操作(未提交事务不会写入 binlog)
具体格式分三种:
-
STATEMENT 格式
- 记录执行的 SQL 语句(如 update t set a=20 where id=1)
- 有SQL歧义, 有可能id =1 的数据不存在
-
ROW 格式(默认推荐)
- 记录行数据的变更前后状态(如「id=1 的行,a 从 10 改成 20」)
- 无 SQL 歧义,复制更精准;
-
MIXED 格式
- 自动切换 STATEMENT/ROW 格式
- 简单 SQL 用 STATEMENT,复杂 SQL 用 ROW
刷盘策略
控制 binlog 从内存(binlog cache)刷到磁盘的时机:
-
sync_binlog=0(默认):
- 由操作系统决定刷盘时机(可能丢失未刷盘的 binlog);
- 非核心业务(如日志存储、测试环境、内部管理系统)
-
sync_binlog=1(生产推荐,最安全):
- 事务提交时强制刷盘,确保 binlog 持久化;
- 金融、支付、核心交易业务
-
sync_binlog=N(N>1):
- 累计 N 个事务后批量刷盘(平衡性能与安全性)
- 高并发写入的普通业务
核心作用
主从复制
主库的 binlog 记录所有数据修改操作
从库通过复制主库的 binlog 并执行,实现主从数据一致
是 MySQL 读写分离、负载均衡、高可用架构(如 MGR、主从切换)的基础
时间点恢复
当数据发生误操作(如误删表、误更新)时,
可通过「全量备份 + binlog 增量恢复」
将数据恢复到误操作前的任意时间点,避免数据丢失
补充事务一致性
与 redo log 通过「两阶段提交」协作,确保主从复制时 binlog 与 redo log 数据一致,避免主从数据差异
上面提到的时间点恢复具体步骤是怎么样的呢?两阶段提交又是怎么回事?
- 通过binlog如何恢复数据?
- 两阶段提交是怎么回事?
跟强子学习一下,有备无患,说不定哪天就用上了~
binlog恢复增量数据
核心前提
- MySQL 已开启 binlog(必须,增量恢复依赖 binlog 记录变更);
- 存在一份 有效全量备份(备份时记录了对应的 binlog 文件名和位置,作为增量恢复的起点);
- binlog 日志文件未被删除 / 覆盖(需提前配置 binlog 保留策略)
记得检查生产环境是否都符合~不然神仙难救~~
核心逻辑
本质是:用全量备份恢复到某个 基准时间点,
再通过 binlog 重放该时间点之后的所有数据变更(增删改)
最终恢复到目标状态
前期准备
环境说明
- MySQL 版本:5.7/8.0(兼容);
- 目标数据库:test_db(需恢复的数据库);
- 全量备份工具:MySQL 自带 mysqldump(无需额外安装,最常用);
- 增量恢复工具:MySQL 自带 mysqlbinlog(解析 binlog 并执行恢复)。
如何确认 binlog 已开启?
# 登录 MySQL 执行
mysql -u root -p
mysql> show variables like 'log_bin'; # 结果 Value 为 ON 表示已开启
mysql> show variables like 'binlog_format'; # 推荐 ROW 格式(恢复更精准,避免 SQL 兼容性问题)
mysql> show master status; # 查看当前 binlog 文件名(如 binlog.000005)和当前位置(Position,如 154)
未开启 binlog?如何配置?
编辑 MySQL 配置文件 my.cnf
[mysqld]
log_bin = /var/lib/mysql/binlog # binlog 日志文件名前缀(路径与 datadir 一致即可)
binlog_format = ROW # 必选,基于行的格式,恢复无歧义
server-id = 1 # 主从架构必填,单机可随便设一个非 0 整数
expire_logs_days = 7 # binlog 保留 7 天(避免被自动删除,根据需求调整)
重启 MySQL 生效
systemctl restart mysqld
完整恢复流程
时间线是怎么样的?
- T0(10:00):全量备份 test_db 数据库(基准备份);
- T1(10:00~14:00):执行一系列数据变更(插入、更新数据);
- T2(14:00):误操作删除 test_db.test_table 表,需恢复到 T2 之前的状态
操作步骤是怎么样的?
先删除损坏的数据库(避免冲突,谨慎操作!),记得备份做好
drop database if exists test_db;
接着准备好恢复资料:
- 确定 全量备份sql文件 比如 test_db_full_20251201_1000.sql
执行全量备份sql ,验证全量恢复结果
- T0 前的 2 条数据
- T1 的变更未恢复
提取增量 binlog
- 需要找到对应的最后执行的sql定位所在的binlog,比如binlog.000005
- 确定备份对应的 binlog 位置:154(增量恢复需从该位置开始)
查看 binlog 日志,找到 “误操作前的终点位置”
# 解析 binlog 文件,查看所有操作(按时间排序)
mysqlbinlog --base64-output=decode-rows -v /var/lib/mysql/binlog.000005
输出示例
# at 154 # 全量备份对应的起点位置
#251201 10:05:23 server id 1 end_log_pos 219 CRC32 0x... Query thread_id=1 exec_time=0 error_code=0
SET TIMESTAMP=1733052323/*!*/;
insert into test_table (name) values ('王五') # 第一条增量数据
/*!*/;
# at 219
#251201 10:06:10 server id 1 end_log_pos 284 CRC32 0x... Query thread_id=1 exec_time=0 error_code=0
SET TIMESTAMP=1733052370/*!*/;
insert into test_table (name) values ('赵六') # 第二条增量数据
/*!*/;
# at 284
#251201 10:07:30 server id 1 end_log_pos 359 CRC32 0x... Query thread_id=1 exec_time=0 error_code=0
SET TIMESTAMP=1733052450/*!*/;
update test_table set name = '张三_更新' where id = 1 # 第三条增量数据
/*!*/;
# at 359
#251201 14:00:00 server id 1 end_log_pos 424 CRC32 0x... Query thread_id=1 exec_time=0 error_code=0
SET TIMESTAMP=1733065200/*!*/;
drop table test_table # 误操作(需停止在该位置之前)
/*!*/;
- 增量恢复的起点:154(全量备份对应的位置);
- 增量恢复的终点:359(误操作 drop table 之前的位置)
应用增量 binlog 恢复
使用 mysqlbinlog 命令解析并执行增量部分的 binlog
# 从起点 154 到终点 359,执行 binlog 中的变更
mysqlbinlog --start-position=154 --stop-position=359 /var/lib/mysql/binlog.000005 | mysql -u root -p
验证恢复结果,看结果是否恢复到误操作前的状态
关键工具与命令汇总
| 操作场景 | 工具 / 命令 |
|---|---|
| 全量备份 | mysqldump -u root -p --master-data=2 --single-transaction --databases 库名 > 备份文件.sql |
| 查看备份的 binlog 位置 | grep "CHANGE MASTER TO" 备份文件.sql |
| 全量恢复 | mysql -u root -p < 备份文件.sql |
| 解析 binlog(查看内容) | mysqlbinlog --base64-output=decode-rows -v binlog文件 |
| 增量恢复(按位置) | `mysqlbinlog --start-position = 起点 --stop-position = 终点 binlog 文件 |
| 增量恢复(按时间) | `mysqlbinlog --start-datetime="2025-12-01 10:00:00"--stop-datetime="2025-12-01 14:00:00" binlog 文件 |
注意事项
-
binlog 必须开启且格式为 ROW:
- 避免使用 STATEMENT 格式(可能因 SQL _mode 差异导致恢复失败);
- 查看格式:show variables like 'binlog_format';,需设为 ROW。
-
全量备份需记录 binlog位置:
- 必须加 --master-data=2 参数,否则无法确定增量恢复的起点;
- 若备份时未加该参数,需通过 show master status 手动记录备份时的 binlog 文件名和位置。
-
binlog 文件不能丢失:
- 配置 expire_logs_days 保留足够长时间的 binlog(如 7~30 天);
- 重要场景可定期归档 binlog 到异地存储。
-
恢复前备份当前数据:
- 恢复前若数据库仍有残留数据,建议先备份(如 mysqldump -u root -p --databases 库名 > 临时备份.sql),避免恢复失败导致数据二次丢失。
-
大事务处理:
- 若增量 binlog 包含大事务,恢复时可能占用较多资源,建议在业务低峰期执行。
两阶段提交
是协调redo log和binlog一致性的核心机制
事务提交时,redo log 的写入会拆成两个阶段,而非一次性提交,最终保证两个日志的记录完全同步
具体过程
第一阶段做了什么操作?
- 事务执行完所有 SQL,InnoDB 写入该事务的所有 redo log 记录
- 把 redo log 的状态标记为Prepare(准备就绪);
- 此时事务未真正提交,redo log 已持久化,但 binlog 还没写
第二阶段做了什么操作?
- Server层写入该事务的 binlog,并刷盘持久化;
- 将 redo log 的状态从Prepare改为Commit;
- 事务正式提交完成
两阶段提交的原因
MySQL 中 redo log 和 binlog 是 两个独立的日志系统,用途不同但必须保持一致:
- redo log:InnoDB 层的物理日志,用于崩溃恢复(保障 crash-safe);
- binlog:Server 层的逻辑日志,用于主从复制、数据备份恢复
如果不做两阶段提交,直接 一次性写日志,会出现两种致命的不一致场景:
场景 1:先写 redo log,再写 binlog(宕机在 binlog 写入前)
-
结果:redo log 已记录事务,但 binlog 没记录该事务;
-
问题
- 主从复制时,从库没同步到这个事务,导致主从数据不一致;
- 用 binlog备份恢复时,也会丢失该事务
场景 2:先写 binlog,再写 redo log(宕机在 redo log 写入前)
-
结果:binlog 已记录事务,但 redo log 没记录;
-
问题
- 崩溃恢复时,InnoDB 找不到该事务的 redo log,会回滚事务,导致主库数据丢失
- 但从库 / 备份有该数据,依然不一致
结论:两阶段提交的核心目的,是让 redo log 和 binlog 要么 都成功写入,要么 都不写入,避免因宕机导致的双日志不一致
binlog 与 redo log 差异
| 特性 | binlog | redo log |
|---|---|---|
| 所属层 | MySQL 服务器层(所有引擎通用) | InnoDB 存储引擎层(仅 InnoDB) |
| 日志类型 | 逻辑日志(SQL / 行变更) | 物理日志(数据页修改) |
| 核心作用 | 主从复制、时间点恢复 | 崩溃恢复、保证事务持久性 |
| 写入方式 | 追加写(可轮转,无限增大) | 循环写(固定大小,覆盖旧日志) |
| 刷盘时机 | 事务提交后(按 sync_binlog 策略) | 事务提交时(按 innodb_flush_log_at_trx_commit 策略) |
| 记录范围 | 所有数据库的修改操作 | 仅 InnoDB 表的数据页修改 |
| 恢复场景 | 误操作恢复、跨库同步 | 数据库崩溃后的数据恢复 |
undo log
是ACID 中的 A ,原子性
文件格式 & 内容
物理存储
InnoDB 引擎层的 逻辑日志 ,二进制内容
它没有独立的 专属文件,而是嵌入在 InnoDB 表空间中
-
共享表空间(默认,MySQL 5.7 及之前):存储在 ibdata1 文件中(与数据字典、undo 段、临时表空间等共用);
-
独立 undo 表空间(推荐,MySQL 8.0 默认开启)
- 通过参数 innodb_undo_tablespaces 配置,生成独立文件 undo_001、undo_002(默认 2 个,可扩展),
- 存储路径由 innodb_undo_directory 指定(默认与 datadir 一致)
逻辑存储结构
undo log 在表空间中以 段 - 区 - 页 的层级结构存储:
- undo段(Undo Segment):每个事务会分配一个或多个 undo 段,用于存储该事务的所有 undo 记录;
- undo页(Undo Page):undo 段由多个连续的数据页组成(默认页大小 16KB),undo 记录按顺序追加写入;
- 回滚段(Rollback Segment):InnoDB 默认有 128 个回滚段(参数 innodb_rollback_segments 控制),每个回滚段可管理多个 undo 段,用于复用资源、减少碎片
特性
undo log 是 循环写入、可回收 的:
- 事务提交后,undo log 不会立即删除(MVCC 可能需要读取历史版本);
- 当 undo log 对应的 历史版本 不再被任何事务引用时,InnoDB 的 purge 线程会异步清理这些过期 undo 记录,释放页空间供新事务复用;
内容
核心元数据
- 事务 ID(TRX_ID):所属事务的唯一标识,用于关联事务和 undo 记录;
- 回滚指针(ROLL_PTR):指向当前记录的上一个版本的 undo 记录(形成 “版本链”,支撑 MVCC 读取);
- 表 ID(TABLE_ID):标识操作的目标表(InnoDB 内部表唯一标识,非用户可见的表名);
- 操作类型(OP_TYPE):标记操作类型(如 INSERT、UPDATE、DELETE);
- 主键 / 唯一键信息:定位被操作的行(如主键值,用于精准回滚时找到目标记录)
不同操作的 undo 记录内容
-
INSERT
- 核心内容:仅记录 插入行的主键值(无需记录其他字段);
- 回滚逻辑:根据主键直接删除这条插入的行
- 特点:事务提交后,这类 undo 记录最容易被 purge 清理
-
UPDATE
- 核心内容:记录 被修改字段的 “旧值”(仅修改前的原始值,不记录新值)+ 主键;
- 回滚逻辑:用旧值覆盖当前字段的新值,恢复到修改前状态
-
DEL
- 核心内容:记录 被删除行的完整字段值(所有列的原始数据)+ 主键;
- 回滚逻辑:重新插入这条完整记录
- 注意:InnoDB 的 DELETE 是 标记删除(逻辑删除),事务提交后,该行不会立即从数据页中物理删除,而是等待 purge 线程根据 undo log 清理
工作流程
undo log 的工作流程
- 事务执行时生成
- 事务回滚时应用
- MVCC 读时遍历
- 过期后清理
四个核心环节展开
InnoDB 表的每行数据默认包含三个隐藏字段,为 undo log 提供基础:
- DB_TRX_ID:记录最后修改该行的事务 ID;
- DB_ROLL_PTR:指向该行对应的 undo log 记录(版本链指针);
- DB_ROW_ID:若表无主键或唯一索引,自动生成的行唯一标识
事务执行时
假设执行事务 (初始 id=1 的行 a=10)
begin;
update t set a=20 where id=1;
update t set a=30 where id=1;
commit;
-
事务启动,分配唯一事务ID(如 trx_id=100)
-
第一次 update(a=10→20)
- 读取 id=1 的数据页到 Buffer Pool;
- 生成一条 undo log 记录(类型:UPDATE,包含旧值 a=10、DB_TRX_ID=100、上一版本指针 null);
- 将该 undo log 写入 undo 表空间(同时生成 redo log 记录 undo log 的修改,确保持久化);
- 更新数据行的 DB_TRX_ID=100,DB_ROLL_PTR 指向刚生成的 undo log 记录;
- 修改内存数据页的 a 值为 20(脏页)
-
第二次 update(a=20→30)
- 生成新的 undo log 记录(类型:UPDATE,包含旧值 a=20、DB_TRX_ID=100、上一版本指针→第一次的 undo log 记录);
- 写入 undo 表空间并记录 redo log;
- 更新数据行的 DB_ROLL_PTR 指向本次的 undo log 记录;
- 修改内存数据页的 a 值为 30(脏页)
-
此时,id=1 行的版本链为:数据页当前值(a=30,trx_id=100)→ undo log2(a=20)→ undo log1(a=10)
事务回滚时
若上述事务执行第二次 update 后,因业务错误执行 rollback;
- InnoDB 读取该事务生成的所有 undo log 记录(按生成顺序反向遍历:先 undo log2,再 undo log1);
- 应用 undo log2:将数据行的 a 值从 30 改回 20;
- 应用 undo log1:将数据行的 a 值从 20 改回 10;
- 清空该事务的 undo log 标记(后续由 purge 线程清理);
- 事务回滚完成,数据恢复到事务开始前的状态(a=10)
MVCC 读时
假设事务 A(trx_id=200)在事务 B(trx_id=100)执行 update 期间,以「可重复读」隔离级别读取 id=1 的行:
- 事务 A 启动时,InnoDB 会记录当前 活跃事务 ID 列表(此时包含 trx_id=100);
- 事务 A 读取 id=1 的数据行,发现其 DB_TRX_ID=100, 属于活跃事务,不可直接读取;
- 通过 DB_ROLL_PTR 遍历版本链,找到上一版本的 undo log2(trx_id=100,仍活跃);
- 继续遍历到 undo log1(trx_id=100,仍活跃,不可直接读取)
- 再往上得到初始版本,无事务 ID,确认初始版本(a=10)对事务 A 可见(无冲突),返回 a=10 给事务 A
- 即使事务 B 后续提交(trx_id=100 变为非活跃),事务 A 再次读取时,仍通过版本链找到初始版本 a=10,保证 可重复读
undo log 清理
- 事务提交后,其生成的 undo log 被标记为 过期(但不会立即删除,因为可能有其他事务在读取该版本链);
- InnoDB 的 purge 线程是后台线程,定期扫描 undo 表空间,筛选出 所有活跃事务都不再访问 的过期 undo log
- 删除这些 undo log 记录,释放 undo 表空间的存储空间,实现循环复用
核心作用
保证事务原子性
- 事务执行过程中,若发生错误(如 SQL 执行失败、手动 rollback、系统崩溃)
- 可通过 undo log 撤销事务已执行的修改,恢复到事务开始前的状态
- 确保事务 要么全做,要么全不做
支撑 MVCC
-
为读取操作提供 历史数据版本:
- 当其他事务修改了数据,当前事务可通过 undo log 遍历历史版本链,读取到事务开始前或指定版本的数据
- 实现 读不加锁、写不阻塞读的并发控制
- 避免脏读、不可重复读
mvcc
MVCC(Multi-Version Concurrency Control,多版本并发控制)
是一个机制
是 InnoDB 存储引擎的核心并发控制机制
核心定位:在不依赖悲观锁的前提下,实现 读不加锁、写不阻塞读 的高并发读写
其底层依赖
- undo log 构建的版本链
- 事务 ID 机制
核心作用
解决读写冲突
MVCC 通过提供数据的 历史版本,让读操作访问旧版本、写操作修改新版本
保障事务隔离性
-
ACID 中的 I
-
是 InnoDB 实现「读已提交(RC)」和「可重复读(RR)」的核心技术:
- 读已提交(RC):每次查询都获取最新的 “已提交事务版本”,避免脏读;
- 可重复读(RR):事务启动时生成数据快照,后续查询仅访问该快照,避免脏读和不可重复读
避免脏读、不可重复读
通过版本链筛选 对当前事务可见的数据版本,
- 读操作不会获取未提交事务的修改(避免脏读),
- 同一事务内多次读取结果一致(避免不可重复读)
核心特点
非阻塞读(快照读)
- 普通查询(如 select * from t where id=1)属于快照读,无需加锁,直接通过版本链获取历史数据,不会阻塞写操作;
- 同时写操作也不会阻塞快照读
什么是快照读?什么是当前读?
-
快照读:普通 select(不加锁),基于 MVCC 访问历史版本,非阻塞;
-
当前读
- 加锁查询(如 select ... for update)、update、delete、insert,
- 访问数据的最新版本,需加锁保证原子性,会阻塞其他写操作
基于版本链实现
依赖于
- InnoDB 数据行的隐藏字段(DB_TRX_ID、DB_ROLL_PTR)
- undo log 构建 版本链
最新数据存储在数据页,历史版本串联在 undo log 中,读操作通过遍历版本链找到可见版本
事务快照(Read View)
每个事务启动时(或查询时,因隔离级别而异)会生成一个「Read View(读视图)」
Read View 是判断版本可见性的依据,包含 4 个关键信息:
- m_ids:当前活跃的事务 ID 列表(未提交的事务);
- min_trx_id:活跃事务 ID 中的最小值;
- max_trx_id:当前已分配的最大事务 ID + 1(下一个要分配的事务 ID);
- creator_trx_id:生成该 Read View 的事务 ID(当前事务 ID)。
工作流程
遍历版本链时,通过数据版本的 DB_TRX_ID(修改该版本的事务 ID)与 Read View 对比,判断是否可见
-
若 DB_TRX_ID == creator_trx_id:当前事务修改的版本,可见;
-
若 DB_TRX_ID < min_trx_id:修改该版本的事务已提交(早于所有活跃事务),可见;
-
若 DB_TRX_ID >= max_trx_id:修改该版本的事务是未来启动的(晚于当前 Read View 生成),不可见;
-
若 min_trx_id <= DB_TRX_ID < max_trx_id:
- 若 DB_TRX_ID 在 m_ids 中(事务未提交),不可见;
- 若 DB_TRX_ID 不在 m_ids 中(事务已提交),可见;
-
若当前版本不可见,通过 DB_ROLL_PTR 遍历上一个版本,重复上述判断,直到找到可见版本(或版本链结束,返回空)
不同隔离级别的流程差异
可重复读
事务启动时生成一次,整个事务生命周期内复用该 Read View
流程示例
-
事务 A(trx_id=100)启动,生成 Read View:m_ids=[100],min_trx_id=100,max_trx_id=101;
-
事务 B(trx_id=101)启动,执行 update t set a=20 where id=1(初始 a=10),提交;
-
事务 A 执行 select a from t where id=1:
- 数据页当前版本 DB_TRX_ID=101,判断:101 >= max_trx_id(101),不可见;
- 遍历 undo log 找到上一版本(a=10,DB_TRX_ID=0,初始版本);
- 0 < min_trx_id(100),可见,返回 a=10;
-
事务 A 再次查询,仍复用同一个 Read View,返回 a=10(可重复读)
读已提交
Read View 生成时机:每次执行查询时生成新的 Read View;
流程示例
-
事务 A(trx_id=100)启动,第一次查询 select a from t where id=1:
- 生成 Read View:m_ids=[100],min_trx_id=100,max_trx_id=101;
- 数据页版本 DB_TRX_ID=0,可见,返回 a=10;
-
事务 B(trx_id=101)启动,执行 update t set a=20 where id=1,提交;
-
事务 A 第二次查询 select a from t where id=1:
- 生成新的 Read View:m_ids=[100],min_trx_id=100,max_trx_id=102;
- 数据页当前版本 DB_TRX_ID=101,判断:101 不在 m_ids 中(事务 B 已提交),可见;
- 返回 a=20(读已提交,每次查询获取最新已提交版本)
当前读的处理逻辑
当前读(select ... for update、update、delete 等)不依赖 MVCC 的快照,而是直接访问数据的最新版本,同时加行锁 / 表锁保证原子性:
- 执行当前读时,先获取目标数据行的排他锁(或共享锁),阻塞其他写操作;
- 直接读取数据页的最新版本(忽略历史版本);
- 执行修改操作后,更新数据行的 DB_TRX_ID 和 DB_ROLL_PTR,生成新的 undo log 记录,更新版本链。
总结
这次解析了与 日志 相关的很多知识点:
- redo log /undo log / binlog
- 两阶段提交 /double write /write-aheard-logging (WAL)
- MVCC
不单单分析了日志的内容,格式,甚至还介绍了一下数据恢复的方法~
深入研究底层原理,未来解决问题会更精准~
熟练度刷不停,知识点吃透稳,下期接着练~