24.1 ACID 与本地事务

一句话

ACID 里只有 A、I、D 是数据库能保证的,C(一致性)是业务语义;而 A/I/D 全靠三样东西:日志(原子 + 持久)、锁或 MVCC(隔离)、回滚段(撤销)。

四个特性靠什么实现

特性 机制 代价
A 原子性 undo/回滚段:失败时撤销已改的页 写 undo,空间与 IO
C 一致性 约束(唯一键、外键)、业务逻辑 不是数据库单方面能保证
I 隔离性 锁(悲观)或 MVCC(乐观) 并发度与复杂度
D 持久性 WAL:先写日志再刷页,提交时 fsync 每次提交一次 fsync(~百 μs–ms)

隔离级别与它允许的异常

级别 脏读 不可重复读 幻读 写偏斜
Read Uncommitted 可能 可能 可能 可能
Read Committed 不可能 可能 可能 可能
Repeatable Read 不可能 不可能 视实现(MySQL 间隙锁可防) 可能
Serializable 不可能 不可能 不可能 不可能

⚠️ “RR 就没有幻读” 只对部分实现成立(MySQL InnoDB 用间隙锁;PostgreSQL 的 RR 是快照隔离,仍允许写偏斜)。

本地事务的边界

边界内 边界外
单库、单连接内的多条 SQL 跨库、跨服务、跨消息队列
由数据库的锁/MVCC/日志保证 需要 2PC、Saga、消息表等(见 24.9 事务模型选型)

代码:一次提交到底做了什么

BEGIN;
  UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 写 undo + 脏页
  UPDATE accounts SET balance = balance + 100 WHERE id = 2;
  INSERT INTO transfers(...) VALUES (...);
COMMIT;   -- ① 写 commit 记录到 WAL ② fsync ③ 返回成功 ④ 后台刷脏页
# 看提交是否真的落盘(PostgreSQL)
SHOW synchronous_commit;      -- on = 每次提交 fsync;off = 可能丢最近事务
# 批量导入时降低 fsync 频率(用事务包住,而不是每条一次)

反模式

反模式 后果
事务里做 RPC/HTTP 调用 事务变长,锁持有久,超时导致回滚风暴
长事务 阻止 MVCC 清理,表膨胀(见 25.2 MVCC)
依赖 “默认隔离级别” 不说清 不同库行为不同,跨库逻辑出错
把 C 当数据库保证 约束缺失时数据仍 “合法” 但业务错

| 用 SELECT ... FOR UPDATE 做长任务 | 锁等待扩散,连接池耗尽 | | 事务里 sleep 重试 | 持锁时间随重试线性增长 |

三条铁律

🔑 事务只包 “必须一起成功” 的操作;RPC 与长计算放到事务外。

🔑 提交 = fsync;批量场景要合并提交,别每条一次。

🔑 隔离级别要显式声明;靠默认值等于靠运气。

自检

Q1 为什么 “提交很慢” 常常是 fsync 的锅?

▸ 答案:因为持久性要求每次提交都把 WAL 刷到磁盘,机械盘上一次 fsync 约 5–10ms,SSD 约 0.1–1ms;每秒 1000 次提交就吃掉大量 IOPS。解法是批量提交/组提交(group commit),把多个事务的日志一次刷盘。

Q2 原子性靠 undo,持久性靠 WAL,两者矛盾吗?

▸ 答案:不矛盾,它们是两个方向:WAL 记录 “要做什么”(redo),保证已提交的能重放;undo 记录 “怎么撤销”,保证未提交的能回滚。崩溃恢复时先 redo 再 undo,得到 “已提交都在、未提交都不在”。

Q3 Serializable 为什么性能差?

▸ 答案:因为它要消除所有异常(含写偏斜),通常靠范围锁/谓词锁或 SSI(可串行化快照隔离)的冲突检测:锁范围大、冲突检测会回滚事务。代价是高竞争下重试率上升。

Q4 写偏斜是什么,RC/RR 为什么挡不住?

▸ 答案:两个事务各读一个集合、各自写另一个集合,单看没有冲突(写不同行),组合起来违反约束(如 “至少一名医生值班”)。快照隔离下它们读到同一快照、写不同行,提交都成功——只有 Serializable 的范围/谓词冲突检测才能发现。

Q5 本地事务能跨消息队列吗?

▸ 答案:不能。发消息与本地提交是两个系统,无法原子。工程上用本地消息表(同一库内写 “待发消息”,再异步投递)或事务消息(见 24.6 本地消息表、24.7 事务消息 (半消息 / 回查))。

Q7 synchronous_commit=off 能提升吞吐,它牺牲了什么?

▸ 答案:牺牲最近若干事务的持久性:崩溃可能丢掉最后几百毫秒已 “提交成功” 的事务(但仍保证一致性,不会出现半条事务)。适合可重放的批量导入,不适合账务。这就是 “D 可以被放松,A 和 I 不能” 的具体体现。

关联