news 2026/10/5 11:36:55

MySQL 事务与 MVCC 深度剖析:从隔离级别到三大日志,一文打通并发与持久化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL 事务与 MVCC 深度剖析:从隔离级别到三大日志,一文打通并发与持久化

事务、MVCC 与三大日志

一条 UPDATE 语句,如何保证"要么全成功、要么全回滚"?并发事务之间为什么不会互相踩?
答案藏在三层结构里:事务隔离级别(语义层)→ MVCC 多版本控制(实现层)→ Redo/Undo/Binlog(持久层)。


一、事务与隔离级别

1.1 ACID

特性含义依赖
原子性 (A)事务不可分割,失败全部回滚undo log
一致性 ©执行前后数据合法由 A/I/D 共同保证
隔离性 (I)并发事务互不干扰MVCC + 锁
持久性 (D)提交后永久持久化redo log

1.2 三大读问题

异常定义直观表现
脏读读到其他事务未提交的数据数据可能回滚,读到"废数据"
不可重复读同一事务内两次读同一条记录,值变化其他事务修改并提交了该行
幻读同一事务内两次查询,结果集行数变化其他事务插入或删除了新行

1.3 四种隔离级别

隔离级别脏读不可重复读幻读MySQL 支持
RU 读未提交可能可能可能✓
RC 读已提交解决可能可能✓(互联网生产常选)
RR 可重复读解决解决快照读无 / 当前读 Next-Key Lock 防✓默认
Serializable 串行化解决解决解决✓(性能极差)

关键认知:MySQL 的 RR 在「快照读」下靠 MVCC 冻结快照消除幻读;在「当前读」下靠 Next-Key Lock 防幻读。锁机制见《03_锁机制与死锁防御》。


二、MVCC 多版本并发控制

MVCC 的核心思想:写数据时不覆盖旧数据(写入 undo log),读数据时按规则选择可见的旧版本。读写操作不同版本,互不干扰。

2.1 三大底层组件

组件一:隐藏字段(每行聚簇索引都有)
隐藏字段大小含义
DB_TRX_ID6 字节最后一次修改该行的事务 ID
DB_ROLL_PTR7 字节指向 undo log 中上一个版本的指针
DB_ROW_ID6 字节无主键时 InnoDB 自动生成聚簇索引
组件二:undo log 版本链 —— 数据的"时光机"

每次 UPDATE 前,先将旧值写入 undo log,新行的roll_pointer指向旧版本,形成从新到旧的单向链表。

B+Tree 物理行(只存最新版本) ┌───────────────────────────────────────┐ │ id=1, status=99, trx_id=10, │ │ roll_pointer ─────────────────────────┼───┐ └───────────────────────────────────────┘ │ ▼ undo log 版本1 ┌──────────────────────┐ │ status=0, trx_id=5, │ │ roll_pointer=NULL │ └──────────────────────┘

存储澄清:B+树叶子节点只存当前最新版本,历史版本链在 undo log 中。MySQL 8.0 默认存储在datadir下的undo_001、undo_002(独立.ibu文件)。

组件三:ReadView —— 判断"谁可见"的规则快照

ReadView 是纯内存结构,事务提交即销毁。包含 4 个字段:

字段含义边界
m_ids生成时活跃(未提交)事务 ID 列表有序数组
min_trx_idm_ids 中最小的事务 ID左边界
max_trx_id下一个即将分配的事务 ID右边界
creator_trx_id创建该 ReadView 的事务自身 ID当前事务

ReadView 是全局的,不是行级的:从内存全局事务管理器复制整个实例的活跃事务列表。好处是跨表 JOIN 逻辑一致;代价是一个古老长查询会"挟持"所有表的历史版本(见保护伞效应)。

2.2 可见性判定算法(5 条规则,严格优先级)

对某版本行的trx_id = T:

① T == creator_trx_id ? → 可见 ✓(自己能看自己的修改) ② T < min_trx_id ? → 可见 ✓(生成前已提交) ③ T >= max_trx_id ? → 不可见 ✗(生成后才开启的新事务) ④ min_trx_id <= T < max_trx_id ? T 在 m_ids 列表中 ? → 不可见 ✗(生成时仍活跃,未提交) T 不在 m_ids 列表 ? → 可见 ✓(生成时已提交)

ASCII 数轴图解:

事务 ID 数轴 ──────────────────────────────────────────────────────► 0 ────── min_trx_id ─────── 活跃事务区间 ─────── max_trx_id ───► ∞ │ │ ↑ m_ids=[7,9,10] ↑ │ │ │ │ (trx_id 7,9,10 活跃) │ 可见区 │ 活跃区(查m_ids) │ 不可见区 (已提交) │ T=7 在m_ids→不可见 ✗ │ T=11,12→不可见 ✗ T=1,2,5 │ T=8 不在m_ids→可见 ✓ │ (未来事务) → 可见 ✓ │ T=9 在m_ids→不可见 ✗ │ │ T=10在m_ids→不可见 ✗ │

边界纠误:T == min_trx_id不可见(它在活跃列表里);T == max_trx_id不可见(它是未来事务)。

2.3 ReadView 生成时机:RC vs RR 的本质区别

隔离级别ReadView 生成时机效果
RC每次 SELECT都生成新 ReadView每次都能看到最新已提交 → 不可重复读
RR事务内首次 SELECT生成,后续复用ReadView 冻结 → 可重复读

超高频考点:RR 下 ReadView 生成于第一条 SELECT,不是 BEGIN。
强制 BEGIN 时生成:START TRANSACTION WITH CONSISTENT SNAPSHOT;

RC 时间轴图解(两次查询 → 两个 ReadView):

事务A (trx_id=7, RC) 事务B (trx_id=10) │ SELECT #1 │ │ → 生成 ReadView #1 │ │ m_ids=[8,10,12] (B 在活跃列表) │ │ → B+Tree trx_id=10 在 m_ids → 不可见 → 回溯旧版本 0 │ │ UPDATE status=99 / COMMIT │ │ (trx_id=10 从全局活跃列表移除) │ SELECT #2 │ │ → 生成 ReadView #2 ★ RC 每次都新 │ │ m_ids=[8,12] (B 已提交,不在列表) │ │ → B+Tree trx_id=10 → 8≤10<13,查 m_ids → 不在 → 可见 ✓ │ → 返回 status=99 └─ COMMIT ✅ 两次查询结果不同:0 → 99(不可重复读)

RR 时间轴图解(两次查询 → 同一个 ReadView):

事务A (trx_id=7, RR) 事务B (trx_id=10) │ SELECT #1 │ │ → 生成 ReadView #1 ★ RR 只生成一次 │ │ m_ids=[8,10,12] (B 在活跃列表) │ │ → B+Tree trx_id=10 在 m_ids → 不可见 → 回溯旧版本 0 │ │ UPDATE status=99 / COMMIT │ │ (trx_id=10 从全局移除) │ SELECT #2 │ │ → 复用 ReadView #1 ★ 冻结不变! │ │ m_ids=[8,10,12] (B 仍在列表里) │ │ → B+Tree trx_id=10 → 在 m_ids → 不可见 ✗ │ → 回溯旧版本 0 → 可见 ✓ └─ COMMIT ✅ 两次查询结果相同:0 → 0(可重复读 / 快照冻结)

关键:即使 B 真实已 COMMIT,ReadView #1 生成时把 B 记在了 m_ids 里,所以对 A 来说 B 的修改永远不可见。

2.4 快照读 vs 当前读

读模式触发 SQL加锁数据来源隔离级别影响
快照读普通SELECT❌ 不加锁undo log 版本链可见版本RC/RR 体现为可见性差异
当前读SELECT FOR UPDATE/UPDATE/DELETE✅ 加锁B+Tree 物理最新行总读最新;隔离级别影响加锁范围

关键结论:UPDATE/DELETE内部先做当前读读到最新值,不管你显式SELECT查的是多少。

账户余额初始 1000,事务A 快照读两次,事务B 中间扣 100: 事务A(快照读) 事务B(当前写) ├─ SELECT balance → 1000 │ ├─ UPDATE SET balance=900 / COMMIT ├─ SELECT balance → 1000(RR) 或 900(RC) │ └─ UPDATE SET balance = balance - 50 → 内部当前读读到最新 900,减成 850 → 不会基于之前的快照读计算 ★ UPDATE/DELETE 内部的读取是当前读,不管你显式查的是多少

三、三大日志:Redo / Undo / Binlog

3.1 总览对比

日志所属层记录内容性质核心作用写入时机
Redo Log引擎层物理页修改(页号、偏移量、内容)物理操作崩溃恢复(持久性 D)事务执行过程中(Prepare + Commit)
Undo Log引擎层逻辑逆操作(id=1, money=90→100)逻辑操作事务回滚(原子性 A)+ MVCC 快照读执行 DML 时实时写入
BinlogServer 层SQL / 行变更逻辑操作主从复制 + 时间点恢复事务提交时(Redo Prepare 之后,Commit 之前)

核心哲学:日志记录"操作(Events)“,数据文件记录"状态(State)”。只要有一串完整的操作日志,就可以推导出任意时刻的状态。

3.2 WAL 机制:Redo Log 为什么快

  • 刷数据页(脏页):随机 I/O(磁盘寻道慢,毫秒级)
  • 写 Redo Log:顺序 I/O(追加写,微秒级)

WAL(Write-Ahead Logging):先写日志(顺序快),再异步刷脏页(随机慢)。宕机后通过 Redo Log 重放,恢复已提交但未刷盘的数据。

3.3 一条 UPDATE 的完整日志时序

UPDATEuserSETmoney=90WHEREid=1;
步骤时机动作落盘
①执行瞬间写 Undo Log(旧值 money=100)实时
②执行瞬间加 X 行锁 + 间隙锁内存锁
③执行瞬间修改 Buffer Pool(money=90),标记脏页仅内存
④执行瞬间写 Redo Log(Prepare 状态)实时
⑤COMMIT时写 Binlog实时
⑥Binlog 写完后写 Redo Log(Commit 状态)实时
⑦提交完成后释放 X 锁—
⑧后台异步脏页刷回磁盘.ibd随机 I/O,慢慢刷

Undo Log 写入时机纠误:Undo Log 是执行 DML 的瞬间就写入的,不是提交时才写。原因:① 其他事务的 MVCC 快照读立刻需要读到旧值;② 用户随时可能 ROLLBACK,必须有 Undo 才能撤销。


四、内部 XA 两阶段提交

4.1 为什么需要

Redo Log(引擎层)和 Binlog(Server 层)是两个独立日志。如果只写其一就宕机:

  • 只写 Redo,未写 Binlog→ 主库恢复有新数据,从库同步缺失 →主从不一致
  • 只写 Binlog,未写 Redo→ 从库重放了数据,主库回滚了 →主从不一致

4.2 两阶段流程

阶段操作状态
PrepareInnoDB 写 Redo Log,标记为PrepareRedo Prepare
Commit① Server 写 Binlog → ② InnoDB 将 Redo 改为CommitRedo Commit

4.3 崩溃恢复规则

宕机时 Redo 状态Binlog 是否存在恢复动作
Prepare不存在回滚(Binlog 没写,提交未完成)
Prepare存在且完整提交(Binlog 写了,Server 认可)
Commit任意提交(事务已确认)

核心规则:以Binlog 写成功作为事务最终提交的"决定性标志"。


五、分布式事务

5.1 为什么单机事务不够

单体事务只在一个数据库连接内生效。涉及多个 MySQL 实例(分库分表)或跨 Redis / MQ,需要分布式事务协调者(TC)介入。

5.2 外部 XA(强一致性,性能差)

XASTART'xid1';-- 代替 BEGINUPDATEuserSETmoney=money-10WHEREid=1;UPDATEuserSETmoney=money+10WHEREid=2;XAEND'xid1';XAPREPARE'xid1';-- 第一阶段:所有参与者 Prepare,锁不释放XACOMMIT'xid1';-- 第二阶段:全局提交(锁释放)

XA PREPARE后行锁一直持有,直到全局XA COMMIT才释放。强一致但性能极差,互联网大厂基本不用。

5.3 Seata AT 模式(最终一致性,高性能)

核心思想:一阶段直接提交本地事务,释放锁;二阶段如果失败,用undo_log表反向补偿。

维度单体 InnoDB Undo LogSeata AT 的undo_log表
存储位置InnoDB 系统 Undo 表空间(二进制)业务数据库中的普通表
记录内容物理/逻辑逆操作SQL 前后镜像(before/after)
作用范围单机本地回滚 + MVCC全局分布式回滚
生命周期Purge 线程自动清理TC 在全局提交后删除
谁生成InnoDB 引擎自动Seata 框架自动(业务无感)

Seata AT 一阶段(业务代码只需@GlobalTransactional):

-- 业务代码:@GlobalTransactionalUPDATEuserSETmoney=money-10WHEREid=1;-- 框架自动执行(同一本地事务中):-- 1. 查询 Before Image(修改前数据)-- 2. 执行业务 UPDATE-- 3. 查询 After Image(修改后数据)-- 4. INSERT INTO undo_log (xid, branch_id, before_image, after_image) VALUES (...)-- 5. COMMIT(释放本地锁)★ 与外部 XA 的核心差异

二阶段:

  • 全局成功 → 框架异步删除undo_log记录
  • 全局失败 → 框架读取before_image,生成反向 SQL 恢复数据

单体日志是分布式事务的"物理保险柜":Seata 的undo_log写入磁盘时,必须依赖 Redo Log 保证不丢失、Binlog 保证同步到从库。如果 Redo Log 不起作用,Seata 刚写完undo_log就宕机,重启后记录丢失 → 无法恢复。

5.4 分布式事务角色

角色全称职责
TCTransaction Coordinator全局大管家,独立部署
TMTransaction Manager开启/提交/回滚全局事务
RMResource Manager执行并汇报本地事务状态

核心速查表

RC vs RR 对比

维度RCRR(MySQL 默认)
解决的读问题脏读脏读 + 不可重复读
ReadView 生成每次 SELECT首次 SELECT
并发修改后可见性立即可见不可见(回溯旧版本)
典型现象0 → 99(不可重复读)0 → 0(快照冻结)
锁机制行锁,几乎不加间隙锁Next-Key Lock(见锁专题)
死锁概率较低较高(GAP 锁范围大)
binlog 要求必须 ROW 格式STATEMENT/ROW/MIXED 都支持
Spring 常量READ_COMMITTEDREPEATABLE_READ

三大日志一句话

日志一句话
Undo“后悔药”——回滚和 MVCC 读旧版本都靠它
Redo“保险单”——宕机后重放已提交但未刷盘的数据
Binlog“广播稿”——主从复制和时点恢复

可见性判定口诀

自己可见 → 已提交的老事务可见 → 未来事务不可见 → 活跃列表里查旧账。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 11:33:58

工程车辆目标检测数据集:YOLO格式标注与YOLOv8训练实战

简介&#xff1a;工程车辆目标检测数据集面向建筑工地智能监控、交通物流管理及自动驾驶环境感知等场景&#xff0c;为算法工程师、计算机视觉研究者与工程类院校师生提供即用型训练数据。数据集聚焦混凝土搅拌车、自卸卡车与挖掘机三类核心工程车辆&#xff0c;覆盖真实建筑与…

作者头像 李华
网站建设 2026/10/5 11:33:37

换机照片怎么保留原图?不同设备迁移方法对比,教你少走弯路

换新手机后&#xff0c;照片怎么迁移才比较稳&#xff1f;如果只有几百张照片&#xff0c;直接互传通常就够了&#xff1b;但面对几千、上万张照片&#xff0c;尤其还有 HEIC、DNG、RAW、视频等文件&#xff0c;就不能只看传输速度。真正需要关注的是文件是否完整、原始信息是否…

作者头像 李华
网站建设 2026/10/5 11:31:44

核电复兴的历史机遇:产业链受益顺序与估值陷阱解析

核电这个题目&#xff0c;说实话这几年我一直在跟踪&#xff0c;但真正让我觉得"历史机遇"这四个字不夸张的转折点&#xff0c;是2022年之后那一连串的电力缺口预测修正。我记得2021年前后看各国电网规划报告&#xff0c;主流预期还是"电力需求低速增长、风光补…

作者头像 李华
网站建设 2026/10/5 11:31:25

插件加载失败全解析:从Web boot到Harness的排查实战

很多人可能没想到&#xff0c; plugins 这个看似简单的词&#xff0c;会是程序员搜索频率最高的词之一。热搜里"iar plugins 是干什么的"、"failed to load plugins web boot: 2 entries did not activate"、"harness failed to load plugins"…

作者头像 李华
网站建设 2026/10/5 11:30:58

ArkTS入门:从TypeScript语法到鸿蒙状态管理与声明式UI

1. 类型系统&#xff1a;先打好语法地基1.1 变量声明与常量意识ArkTS是TypeScript的超集&#xff0c;所以在入门阶段&#xff0c;你完全可以按照TS的写法先跑起来&#xff0c;但真正上手项目之后&#xff0c;你会发现ArkTS在类型约束上比纯TS更严格&#xff0c;尤其是对any类型…

作者头像 李华
网站建设 2026/10/5 11:30:45

仿京东数码页面动态效果:CSS动画与JavaScript交互实战指南

简介&#xff1a;这是一份仿京东数码商城的纯前端动态网页源码&#xff0c;共20个文件&#xff0c;包含2个HTML页面、1个CSS样式表以及17张JPG/PNG图片素材&#xff0c;压缩包仅503KB。页面用HTML搭建商品陈列、导航栏等结构&#xff0c;CSS负责响应式布局和悬停、滑动等交互动…

作者头像 李华