写点什么

Java 王者修炼手册【Mysql 篇 - 日志】:吃透 MySQL redo log + undo log + binlog 底层机制

作者:DonaldCen
  • 2025-12-05
    广东
  • 本文字数:10151 字

    阅读完需:约 33 分钟

Java 王者修炼手册【Mysql 篇 - 日志】:吃透 MySQL redo log + undo log + binlog 底层机制

大家好,我是程序员强子。

昨天学习了 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 常 < 1~10MB/s

  • 固态硬盘 相差 2~10 倍,顺序≈500MB/s~1GB/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 -pmysql> 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=0SET 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=0SET 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=0SET 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=0SET 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
复制代码

验证恢复结果,看结果是否恢复到误操作前的状态

关键工具与命令汇总

注意事项

  • 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 差异

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_IDDB_ROLL_PTR,生成新的 undo log 记录,更新版本链

总结

这次解析了与 日志 相关的很多知识点:

  • redo log /undo log / binlog

  • 两阶段提交 /double write /write-aheard-logging (WAL)

  • MVCC

不单单分析了日志的内容,格式,甚至还介绍了一下数据恢复的方法~

深入研究底层原理,未来解决问题会更精准~

熟练度刷不停,知识点吃透稳,下期接着练~

发布于: 刚刚阅读数: 3
用户头像

DonaldCen

关注

有个性,没签名 2019-01-13 加入

跟我在峡谷学Java 公众号:程序员悟空的宝藏乐园

评论

发布
暂无评论
Java 王者修炼手册【Mysql 篇 - 日志】:吃透 MySQL redo log + undo log + binlog 底层机制_Binlog_DonaldCen_InfoQ写作社区