事务
事务是数据库把一组读写操作包装成原子提交、并发隔离与崩溃恢复边界的执行单元。
[!info] related notes
事务
一句话定义
事务是数据库定义的一段“要么作为一个整体生效,要么整体不生效”的执行边界,它同时约束了提交、回滚、并发可见性和崩溃恢复。
它要解决什么问题
如果没有事务,数据库至少会暴露三类核心风险:
- 多步写入只成功一半,留下中间状态
- 并发事务互相穿插,读到彼此未完成的结果,或者把彼此的更新覆盖掉
- 数据还没完整落盘时数据库崩溃,提交成功的结果丢失,未提交的脏修改却残留
事务就是数据库对这三类风险给出的统一抽象:
- 对应用层:你只需要划定“哪些操作必须一起成立”
- 对数据库内核:它要负责把这段操作组织成可提交、可回滚、可恢复、可并发协调的执行单元
[!tip] 事务的本质 为了在这些不可靠条件下,仍然让数据库表现得像一个可靠系统。
核心机制 / 工作原理
1. 事务首先是一条边界
典型事务边界是:
BEGIN- 读取或修改若干行数据
- 成功则
COMMIT - 失败则
ROLLBACK
这条边界表达的是:
- 在
COMMIT之前,事务产生的结果通常不应被当成“最终成立” - 在
COMMIT之后,数据库要把它视为“已经成立的事实” - 如果中途失败,数据库要能撤销这段事务已经做过的修改
很多数据库默认启用 autocommit,这意味着“单条 SQL 语句也可以自成一个事务”。所以事务不等于“必须手写 BEGIN ... COMMIT”,而是数据库一直在维护的一种执行语义。
2. 事务的外部语义通常用 ACID 描述
ACID 是事务对外承诺的四类性质:
- Atomicity:一组操作不能只成功一半
- Consistency:提交前后必须满足约束和业务不变量
- Isolation:并发事务不能随意看到彼此的中间状态
- Durability:提交成功后,结果不能因为崩溃而轻易丢失
其中最容易被误解的是:
- 原子性不是“数据库很聪明”,而是要靠回滚链路和恢复链路实现
- 一致性不是数据库单方面送你的礼物,它还依赖约束设计和应用层是否把业务边界放进同一个事务
- 隔离性不是简单的“开了事务就安全”,还要看 事务隔离级别
- 持久性不是“数据页马上写盘”,而是“数据库已经把恢复所需的信息安全落盘”
3. 提交与回滚背后依赖日志和恢复机制
事务之所以能“失败就撤回、成功后重启还能恢复”,核心不靠魔法,而靠日志先行和恢复流程。
数据库内部通常会区分几类职责:
一个重要原则是 write-ahead logging:
- 先把“足以恢复事务结果”的日志写安全
- 再把真实数据页慢慢刷回磁盘
所以“事务提交成功”的真正含义,通常不是“所有数据页此刻都已经写回磁盘”,而是“即使现在宕机,数据库也已经有能力把这次提交恢复出来”。
4. 隔离性靠调度规则,而不是只靠一个开关
事务除了要处理“要不要提交”,还要处理“多个事务同时运行时怎么互不踩踏”。
数据库常见的协调手段包括:
换句话说,事务不是一段孤立脚本,而是数据库在高并发下安排许多事务“如何交错执行”时的基本单位。
5. 一致性最终体现为“不变量是否被守住”
判断一个事务是否设计合理,不能只看它有没有 COMMIT,更要看它是否把真正的不变量包住了。
例如转账场景中的不变量包括:
- 总金额不能凭空减少或增加
- 账户余额不能违反约束
- 转账流水和余额变更不能脱节
如果“扣钱”在一个事务,“记流水”在另一个事务,那么即使两个事务各自都成功提交,业务整体仍然可能不一致。
最小例子 / 最小场景
BEGIN;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;
这个例子表面上只是在做两条 UPDATE,但事务实际在保证三件事:
- 第二条更新失败时,第一条更新必须被撤销
- 其他并发事务不应该看到“账户 1 已扣钱、账户 2 还没到账”的半成品状态
- 如果
COMMIT返回成功,哪怕数据库随后宕机,重启后也应能恢复出这次转账结果
如果只把事务理解成“失败时回滚”,就会漏掉后两件事。
从原理上理解事务,最该盯住哪三件事
1. 单事务内部是否能全成全败
这是原子性问题。核心在于:
- 修改前的旧值能不能找回
- 回滚路径是否完整
- 错误发生在中间任意步骤时,数据库能不能把现场恢复到事务开始前或合规状态
2. 多事务并发时是否还能像“有秩序地执行”
这是隔离性问题。核心在于:
- 读能看到哪个版本
- 写和写冲突时谁等待、谁失败、谁回滚
- 范围查询如何避免脏读、不可重复读、幻读和丢失更新等异常
理想化目标是“结果等价于某种串行执行顺序”,但实际数据库会在正确性和吞吐之间做权衡。
3. 崩溃后能否恢复到一个正确状态
这是持久性和恢复问题。核心在于:
- 哪些事务已经算提交成功
- 哪些事务必须在恢复阶段被重做
- 哪些未提交事务必须在恢复阶段被撤销
这也是为什么学习事务时绕不开 数据库日志。
事务在数据库内部通常依赖什么
不同数据库实现细节不同,但大体都会依赖下面几层能力:
- 事务管理器:跟踪事务开始、提交、回滚和状态
- 锁管理器:协调并发访问,必要时阻塞、唤醒或检测死锁
- 版本可见性机制:例如 MVCC,决定当前事务能看到哪个版本
- 日志系统:例如 Redo Log、Undo Log、WAL
- 恢复流程:数据库重启时根据日志决定重做和撤销哪些操作
所以事务不是某个单独功能按钮,而是数据库内核多个子系统协同后的结果。
工程上怎么判断“这里该不该开事务”
通常在下面几类场景里应该明确使用事务:
- 多条写操作必须共同成功,例如余额变更、流水记录、状态推进
- 先读再写,且读出来的值会影响后续写入,例如扣库存、抢名额、余额校验
- 某条业务规则必须在一个一致性边界内被验证和落地
同时也要记住事务的工程边界:
- 事务应尽量短,不要把长时间等待、外部 HTTP 调用、用户交互塞进事务里
- 事务只天然覆盖数据库自己能控制的状态
- 如果流程跨数据库、消息队列、缓存或第三方接口,就可能需要分布式事务、补偿机制、Outbox 一类额外设计
边界与易混淆点
- 事务不是只有“回滚”这一件事。它同时涉及原子提交、并发隔离和崩溃恢复。
- 一个请求不一定只包含一个事务;一个事务也不一定只是一条 SQL。autocommit 模式下,单条 SQL 往往也是事务。
- “用了事务”不等于“并发下就绝对安全”。并发安全还取决于 事务隔离级别、MVCC 和 数据库锁。
- 一致性不是数据库替你自动完成全部业务正确性。事务边界划错了,照样会得到“技术上提交成功、业务上仍然错误”的结果。
- 持久性不等于“提交瞬间所有脏页都刷盘”。更准确地说,是数据库已经把恢复所需的信息安全保存下来。
- 长事务通常会放大锁等待、版本膨胀、回滚成本和 死锁 风险,所以“事务越大越稳”是错觉。
- 数据库事务天然擅长处理“本数据库内部状态”的一致性;一旦跨出数据库边界,它就不再是全部答案。