
事务、MVCC 与三大日志一条 UPDATE 语句如何保证要么全成功、要么全回滚并发事务之间为什么不会互相踩答案藏在三层结构里事务隔离级别语义层→ MVCC 多版本控制实现层→ Redo/Undo/Binlog持久层。一、事务与隔离级别1.1 ACID特性含义依赖原子性 (A)事务不可分割失败全部回滚undo log一致性 ©执行前后数据合法由 A/I/D 共同保证隔离性 (I)并发事务互不干扰MVCC 锁持久性 (D)提交后永久持久化redo log1.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 字节最后一次修改该行的事务 IDDB_ROLL_PTR7 字节指向 undo log 中上一个版本的指针DB_ROW_ID6 字节无主键时 InnoDB 自动生成聚簇索引组件二undo log 版本链 —— 数据的时光机每次 UPDATE 前先将旧值写入 undo log新行的roll_pointer指向旧版本形成从新到旧的单向链表。BTree 物理行只存最新版本 ┌───────────────────────────────────────┐ │ id1, status99, trx_id10, │ │ roll_pointer ─────────────────────────┼───┐ └───────────────────────────────────────┘ │ ▼ undo log 版本1 ┌──────────────────────┐ │ status0, trx_id5, │ │ roll_pointerNULL │ └──────────────────────┘存储澄清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 │ 不可见区 (已提交) │ T7 在m_ids→不可见 ✗ │ T11,12→不可见 ✗ T1,2,5 │ T8 不在m_ids→可见 ✓ │ (未来事务) → 可见 ✓ │ T9 在m_ids→不可见 ✗ │ │ T10在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_id7, RC) 事务B (trx_id10) │ SELECT #1 │ │ → 生成 ReadView #1 │ │ m_ids[8,10,12] (B 在活跃列表) │ │ → BTree trx_id10 在 m_ids → 不可见 → 回溯旧版本 0 │ │ UPDATE status99 / COMMIT │ │ (trx_id10 从全局活跃列表移除) │ SELECT #2 │ │ → 生成 ReadView #2 ★ RC 每次都新 │ │ m_ids[8,12] (B 已提交不在列表) │ │ → BTree trx_id10 → 8≤1013查 m_ids → 不在 → 可见 ✓ │ → 返回 status99 └─ COMMIT ✅ 两次查询结果不同0 → 99不可重复读RR 时间轴图解两次查询 → 同一个 ReadView事务A (trx_id7, RR) 事务B (trx_id10) │ SELECT #1 │ │ → 生成 ReadView #1 ★ RR 只生成一次 │ │ m_ids[8,10,12] (B 在活跃列表) │ │ → BTree trx_id10 在 m_ids → 不可见 → 回溯旧版本 0 │ │ UPDATE status99 / COMMIT │ │ (trx_id10 从全局移除) │ SELECT #2 │ │ → 复用 ReadView #1 ★ 冻结不变 │ │ m_ids[8,10,12] (B 仍在列表里) │ │ → BTree trx_id10 → 在 m_ids → 不可见 ✗ │ → 回溯旧版本 0 → 可见 ✓ └─ COMMIT ✅ 两次查询结果相同0 → 0可重复读 / 快照冻结关键即使 B 真实已 COMMITReadView #1 生成时把 B 记在了 m_ids 里所以对 A 来说 B 的修改永远不可见。2.4 快照读 vs 当前读读模式触发 SQL加锁数据来源隔离级别影响快照读普通SELECT❌ 不加锁undo log 版本链可见版本RC/RR 体现为可见性差异当前读SELECT FOR UPDATE/UPDATE/DELETE✅ 加锁BTree 物理最新行总读最新隔离级别影响加锁范围关键结论UPDATE/DELETE内部先做当前读读到最新值不管你显式SELECT查的是多少。账户余额初始 1000事务A 快照读两次事务B 中间扣 100 事务A快照读 事务B当前写 ├─ SELECT balance → 1000 │ ├─ UPDATE SET balance900 / COMMIT ├─ SELECT balance → 1000(RR) 或 900(RC) │ └─ UPDATE SET balance balance - 50 → 内部当前读读到最新 900减成 850 → 不会基于之前的快照读计算 ★ UPDATE/DELETE 内部的读取是当前读不管你显式查的是多少三、三大日志Redo / Undo / Binlog3.1 总览对比日志所属层记录内容性质核心作用写入时机Redo Log引擎层物理页修改页号、偏移量、内容物理操作崩溃恢复持久性 D事务执行过程中Prepare CommitUndo Log引擎层逻辑逆操作id1, money90→100逻辑操作事务回滚原子性 A MVCC 快照读执行 DML 时实时写入BinlogServer 层SQL / 行变更逻辑操作主从复制 时间点恢复事务提交时Redo Prepare 之后Commit 之前核心哲学日志记录操作Events“数据文件记录状态State”。只要有一串完整的操作日志就可以推导出任意时刻的状态。3.2 WAL 机制Redo Log 为什么快刷数据页脏页随机 I/O磁盘寻道慢毫秒级写 Redo Log顺序 I/O追加写微秒级WALWrite-Ahead Logging先写日志顺序快再异步刷脏页随机慢。宕机后通过 Redo Log 重放恢复已提交但未刷盘的数据。3.3 一条 UPDATE 的完整日志时序UPDATEuserSETmoney90WHEREid1;步骤时机动作落盘①执行瞬间写 Undo Log旧值 money100实时②执行瞬间加 X 行锁 间隙锁内存锁③执行瞬间修改 Buffer Poolmoney90标记脏页仅内存④执行瞬间写 Redo LogPrepare 状态实时⑤COMMIT时写 Binlog实时⑥Binlog 写完后写 Redo LogCommit 状态实时⑦提交完成后释放 X 锁—⑧后台异步脏页刷回磁盘.ibd随机 I/O慢慢刷Undo Log 写入时机纠误Undo Log 是执行 DML 的瞬间就写入的不是提交时才写。原因① 其他事务的 MVCC 快照读立刻需要读到旧值② 用户随时可能 ROLLBACK必须有 Undo 才能撤销。四、内部 XA 两阶段提交4.1 为什么需要Redo Log引擎层和 BinlogServer 层是两个独立日志。如果只写其一就宕机只写 Redo未写 Binlog→ 主库恢复有新数据从库同步缺失 →主从不一致只写 Binlog未写 Redo→ 从库重放了数据主库回滚了 →主从不一致4.2 两阶段流程阶段操作状态PrepareInnoDB 写 Redo Log标记为PrepareRedo PrepareCommit① Server 写 Binlog → ② InnoDB 将 Redo 改为CommitRedo Commit4.3 崩溃恢复规则宕机时 Redo 状态Binlog 是否存在恢复动作Prepare不存在回滚Binlog 没写提交未完成Prepare存在且完整提交Binlog 写了Server 认可Commit任意提交事务已确认核心规则以Binlog 写成功作为事务最终提交的决定性标志。五、分布式事务5.1 为什么单机事务不够单体事务只在一个数据库连接内生效。涉及多个 MySQL 实例分库分表或跨 Redis / MQ需要分布式事务协调者TC介入。5.2 外部 XA强一致性性能差XASTARTxid1;-- 代替 BEGINUPDATEuserSETmoneymoney-10WHEREid1;UPDATEuserSETmoneymoney10WHEREid2;XAENDxid1;XAPREPARExid1;-- 第一阶段所有参与者 Prepare锁不释放XACOMMITxid1;-- 第二阶段全局提交锁释放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-- 业务代码GlobalTransactionalUPDATEuserSETmoneymoney-10WHEREid1;-- 框架自动执行同一本地事务中-- 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 对比维度RCRRMySQL 默认解决的读问题脏读脏读 不可重复读ReadView 生成每次 SELECT首次 SELECT并发修改后可见性立即可见不可见回溯旧版本典型现象0 → 99不可重复读0 → 0快照冻结锁机制行锁几乎不加间隙锁Next-Key Lock见锁专题死锁概率较低较高GAP 锁范围大binlog 要求必须 ROW 格式STATEMENT/ROW/MIXED 都支持Spring 常量READ_COMMITTEDREPEATABLE_READ三大日志一句话日志一句话Undo“后悔药”——回滚和 MVCC 读旧版本都靠它Redo“保险单”——宕机后重放已提交但未刷盘的数据Binlog“广播稿”——主从复制和时点恢复可见性判定口诀自己可见 → 已提交的老事务可见 → 未来事务不可见 → 活跃列表里查旧账。