5.1.1 原⼦性

MySQL 原子性(Atomicity)完全指南

原子性是 ACID 的基石。如果说事务是一枚硬币,原子性就是硬币不可分割的物理实体------它确保"全有或全无"(All or Nothing),杜绝"半成品"状态的存在


一、原子性的本质定义

原子性(Atomicity) 指的是:一个事务中的所有操作,在逻辑上是一个不可分割的最小工作单元 。这些操作要么全部执行成功 (提交),要么在遇到任何错误时全部撤销(回滚),绝不允许出现"执行了一部分"的中间状态。

生活中的类比

取款机取钱:扣卡余额 + 吐出现金。如果钱扣了但机器不出钞,银行必须把钱退回去。这个"退回"动作,就是原子性的体现。

SQL 中的直观体现

sql 复制代码
-- 转账:扣钱 + 加钱 必须同生共死
START TRANSACTION;

UPDATE account SET balance = balance - 100 WHERE user_id = 1;  -- 扣钱
UPDATE account SET balance = balance + 100 WHERE user_id = 2;  -- 加钱

-- 如果第二条SQL失败(如账户冻结),第一条必须撤销
COMMIT;  -- 或 ROLLBACK;

二、原子性的底层实现原理:Undo Log

原子性由 Undo Log(回滚日志)独家保障。与 Redo Log 负责"重做"(持久性)不同,Undo Log 专职负责"撤回"(原子性)。

2.1 Undo Log 是什么?

Undo Log 是 InnoDB 存储引擎为每个读写事务生成的历史版本数据链 。在执行任何修改操作(INSERT/UPDATE/DELETE)之前,InnoDB 会先将修改前的旧数据以逻辑日志的形式记录下来。

存储位置 :Undo Log 存储在 Undo 表空间undo_001undo_002)中,默认存放在系统表空间(ibdata1)或独立的 undo 文件中。

2.2 不同操作对应的 Undo 记录

操作类型 Undo Log 记录内容 回滚时的逆向操作
INSERT 记录新行的主键值(PK) 执行 DELETE,根据主键删除新行
DELETE 记录整行的旧数据副本 执行 INSERT,将旧数据原样插回
UPDATE 记录被修改字段的旧值 执行 UPDATE,将字段改回旧值
UPDATE(主键) 记录旧行数据 + 新行主键 拆分为 DELETE 旧行 + INSERT 新行

关键设计差异:INSERT 的 Undo 只记主键(行太新,无需存全量);DELETE 和 UPDATE 必须存完整旧数据,因为要精确复原。

2.3 InnoDB 行记录的隐藏字段

InnoDB 中每行记录(聚簇索引叶子节点)都带有 3 个隐藏字段,它们是 Undo Log 链的核心:

隐藏字段 作用
DB_TRX_ID 最后修改该行的事务 ID
DB_ROLL_PTR(回滚指针) 指向 Undo Log 中该行历史版本的指针,形成版本链
DB_ROW_ID 行 ID(无主键时用于聚簇索引)

版本链示意图

复制代码
当前行(最新版本)
├── DB_TRX_ID = 100
├── DB_ROLL_PTR ──────┐
│                      ▼
│              ┌─────────────────┐
│              │  Undo Log V2    │  ← UPDATE 前的旧值 (trx_id=99)
│              │  DB_ROLL_PTR ───┼──┐
│              └─────────────────┘  │
│                                   ▼
│                           ┌─────────────────┐
│                           │  Undo Log V1    │  ← INSERT 的原始值 (trx_id=98)
│                           │  DB_ROLL_PTR = NULL
│                           └─────────────────┘

三、事务执行全流程中的原子性保障

3.1 正常提交(COMMIT)流程

复制代码
START TRANSACTION;
  │
  ├─① 分配事务ID (trx_id = 100)
  │
  ├─② 执行 UPDATE ... (修改数据页)
  │   ├── 将修改前的旧数据写入 Undo Log (记录 trx_id=100)
  │   ├── 修改 Buffer Pool 中的数据行
  │   └── 将新行的 DB_TRX_ID 设为 100,DB_ROLL_PTR 指向新 Undo
  │
  ├─③ 执行 INSERT ... (插入新行)
  │   └── 将新行的主键写入 Undo Log
  │
  ├─④ 写入 Redo Log (保证持久性,与原子性无关)
  │
  └─⑤ COMMIT
       └── 在 Redo Log 中写入 COMMIT 标记
           事务成功,Undo Log 进入"待清理"状态(但保留用于 MVCC)

3.2 异常回滚(ROLLBACK)流程

当发生以下情况时,触发回滚:

  • 应用层显式执行 ROLLBACK
  • SQL 执行报错(如违反唯一约束、字段溢出)
  • 死锁被检测到,MySQL 自动回滚受害者事务
  • 锁等待超时(innodb_lock_wait_timeout
  • 数据库宕机重启后的恢复阶段

回滚执行过程

复制代码
触发 ROLLBACK / 异常中断
  │
  ├─① 从当前行的 DB_ROLL_PTR 开始,遍历该事务的所有 Undo Log 记录
  │
  ├─② 按照 Undo Log 的逆向顺序执行回滚:
  │   ├── 遇到 INSERT Undo → 执行 DELETE (根据主键删新行)
  │   ├── 遇到 UPDATE Undo → 执行 UPDATE (将字段改回旧值)
  │   └── 遇到 DELETE Undo → 执行 INSERT (将旧行插回)
  │
  ├─③ 将事务状态标记为 TRX_UNDO_PREPARED → ROLLBACK_DONE
  │
  └─④ 释放该事务持有的所有锁
      数据完全恢复至事务开始前的状态。

3.3 崩溃恢复时的原子性保障

MySQL 宕机重启时,会通过 Redo Log + Undo Log 协作进行恢复:

事务状态 恢复策略 实现方式
已提交(COMMIT) 前滚(Redo) 应用 Redo Log,重做已提交的修改
未提交 / 已回滚 后滚(Undo) 应用 Undo Log,撤销未完成事务的修改

这个过程确保了即使数据库在事务执行中途宕机,重启后也不会留下"半截子"数据。


四、容易被忽略的原子性陷阱

4.1 事务中的 DDL 操作(隐式提交)

在 MySQL 中,DDL 语句(CREATE、ALTER、DROP、TRUNCATE)会触发隐式提交,导致当前事务被强制提交,无法回滚。

sql 复制代码
START TRANSACTION;

INSERT INTO users (name) VALUES ('Alice');  -- 可回滚

ALTER TABLE users ADD COLUMN age INT;        -- ❗ 触发隐式 COMMIT

ROLLBACK;  -- 此时只能回滚到 ALTER 之后,Alice 已被永久插入!

强制规范绝对禁止在业务事务中混入 DDL 操作。DDL 应单独执行。

4.2 嵌套事务的误解

MySQL 不支持真正的嵌套事务(SAVEPOINT 除外)。

sql 复制代码
START TRANSACTION;  -- 事务 A

UPDATE t SET x=1;

START TRANSACTION;  -- 实际是隐式 COMMIT 了事务 A,新启事务 B
UPDATE t SET x=2;

ROLLBACK;  -- 只回滚事务 B,x 变成了 1(事务 A 已提交)

4.3 部分回滚(SAVEPOINT)

MySQL 支持通过 SAVEPOINT 实现事务内的部分回滚,但不影响整体原子性逻辑:

sql 复制代码
START TRANSACTION;

INSERT INTO t VALUES (1);
SAVEPOINT sp1;

INSERT INTO t VALUES (2);  -- 假设这里出错
ROLLBACK TO SAVEPOINT sp1; -- 只撤销第二条插入

INSERT INTO t VALUES (3);
COMMIT;  -- 最终插入 1 和 3,(2) 被撤销。整体原子性依然成立。

4.4 回滚失败的可能性

理论上,Undo Log 回滚也可能失败(如磁盘损坏)。但 InnoDB 的设计保证了:

  • Undo Log 本身也受 Redo Log 保护,保证其完整性
  • 如果回滚过程中发生错误,InnoDB 会进入崩溃恢复模式,禁止用户访问,直到问题解决
  • 绝大多数生产环境中的回滚都是成功的

五、原子性 vs 持久性(Undo vs Redo)

这两个概念常被混淆,对比理解更清晰:

对比维度 原子性(Atomicity) 持久性(Durability)
核心问题 事务失败时,如何撤销已做的修改? 事务成功后,如何防止修改丢失?
实现日志 Undo Log(回滚日志) Redo Log(重做日志)
存储内容 修改前的旧数据(逆向操作) 修改后的新数据(正向操作)
应用时机 事务回滚 时 / 崩溃恢复(后滚) 事务提交 时 / 崩溃恢复(前滚)
日志类型 逻辑日志(记录逆向 SQL 逻辑) 物理日志(记录数据页的物理修改)
空间释放 Purge 线程异步回收(需确保无 MVCC 读依赖) 循环写入(覆盖),空间固定

六、原子性对 MVCC 的额外贡献

Undo Log 不仅是回滚工具,还是 MVCC(多版本并发控制) 的数据源。

  • REPEATABLE READ 隔离级别下,事务开始时生成 Read View
  • 读取数据时,通过 DB_ROLL_PTR 沿着 Undo 版本链向后遍历,找到对当前事务可见的历史版本
  • 即使事务提交后,Undo 记录也不能立即物理删除,因为可能有其他长事务还在读取该历史快照
  • 由后台 Purge 线程负责判断:当没有任何 Read View 再需要某个 Undo 版本时,才将其物理清理

七、生产环境最佳实践与监控

✅ 利用原子性的正确姿势

  1. 事务尽量短小:长事务会产生大量 Undo Log,占用磁盘空间,且影响 Purge 效率
  2. 显式控制边界 :始终使用 START TRANSACTION / BEGIN,避免使用 autocommit=1(默认)导致每句都成事务
  3. 捕获异常并回滚 :应用层务必 try-catch,在 catch 中执行 ROLLBACK
  4. 监控 Undo 表空间大小:Undo 膨胀可能导致磁盘写满,定期监控

📊 监控原子性相关指标

sql 复制代码
-- 1. 查看当前运行中的事务(判断是否有长事务产生大量 Undo)
SELECT trx_id, trx_state, trx_started, 
       trx_rows_locked, trx_rows_modified,
       TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec
FROM information_schema.innodb_trx
ORDER BY trx_started;

-- 2. 查看 InnoDB 状态中的 Undo 信息(包括 Purge 进度)
SHOW ENGINE INNODB STATUS\G
-- 重点查看 "TRANSACTIONS" 部分:
-- "History list length" 表示等待 Purge 的 Undo 记录数量,数值过大说明 Purge 跟不上

-- 3. 查看 Undo 表空间大小(MySQL 8.0)
SELECT TABLESPACE_NAME, FILE_NAME, TOTAL_EXTENTS, FREE_EXTENTS
FROM information_schema.INNODB_TABLESPACES
WHERE TABLESPACE_NAME LIKE 'undo%';

⚠️ 常见问题排查

现象 可能原因 解决方案
Undo 表空间无限增大 存在长时间未提交的读事务(如慢查询、备份),导致 Purge 无法清理 查找并终止长事务,优化查询
History list length 持续飙升 写入压力大,Purge 线程处理不及时 调大 innodb_purge_threads,检查是否有大事务
回滚操作极其缓慢 单个事务修改了海量数据(数千万行) 拆分大事务为批量小事务,避免一次性修改过多数据

八、总结

核心要点 关键描述
本质定义 事务内的操作是一个不可分割的原子单位:全成功或全失败
实现技术 Undo Log(回滚日志),记录修改前的旧数据(逆向操作)
回滚机制 通过 DB_ROLL_PTR 遍历 Undo 版本链,逆向执行反向 SQL 恢复数据
额外价值 MVCC 提供历史版本数据,支撑一致性非锁定读
空间管理 Purge 线程异步清理不再需要的 Undo 记录
最大威胁 大事务/长事务产生海量 Undo,导致磁盘爆满和性能雪崩

一句话记住原子性

原子性是事务的"后悔药",通过 Undo Log 记录所有修改前的旧模样,确保无论发生什么意外,数据都能一键复原到起点。

在生产环境中,"短小精悍" 是事务设计的黄金法则------事务越小,原子性保障的成本越低,系统越健壮。

相关推荐
机建狂魔1 小时前
Codex 接入第三方模型 API 实战:以 Mimo 为例
java·服务器·数据库·ai·ai编程·codex
山峰哥2 小时前
数据库工程与SQL调优:从慢查询到秒级响应的实战之路
java·开发语言·数据库·sql·深度优先·启发式算法
moMo2 小时前
# 向量数据库入门:从"关键词匹配"到"语义理解"
数据库
ACP广源盛139246256732 小时前
DeepSeek‑V4‑Flash 公测@ACP#昇腾 950 国产算力组合落地,国产 PCIe 交换芯片 IX9104 有哪些硬件机会
大数据·数据库·人工智能·分布式·单片机·嵌入式硬件·microsoft
进击的女IT3 小时前
通过mysql中的Data目录恢复数据库数据
数据库·mysql
yuezhilangniao3 小时前
AI工具全家桶:从开发到运维,从数据库到产品经理-含魔塔社区简介
运维·数据库·人工智能
gb42152873 小时前
ChromaDB向量数据库的特点
数据库
IvorySQL4 小时前
PostgreSQL 日报| GiST 索引扫描可见性缺陷(8 月 3 日)
数据库·人工智能·postgresql·开源
xiaoxiangsiyan4 小时前
LNMP + Redis Sentinel 高可用架构部署手册(续)
运维·网络·数据库·redis·缓存·架构·sentinel