数据库事务
数据库事务把一组读写放进同一个提交边界,并规定失败恢复、并发可见性和提交后的持久性。
1. 事务保护一组相关状态变化
账户转账至少包含扣减付款方余额与增加收款方余额。如果其中一步提交、另一步失败,数据就会进入不符合业务规则的状态。把两步放进一个本地数据库事务,可以让它们共同提交或共同回滚。
START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;
事务边界还要检查第一条更新是否真正影响一行,并通过约束防止非法余额。事务只能覆盖同一数据库能够协调的操作;HTTP 调用、消息发送和另一个数据库不会自动加入这次提交。
2. ACID 描述四类保证
2.1 原子性 Atomicity
事务中的修改要么提交,要么在失败时不对外生效。InnoDB 使用 undo log 等机制保存回滚所需信息。原子性不表示代码块不会抛异常,而是异常后的数据库状态能够回到未提交结果不可见的状态。
2.2 一致性 Consistency
事务执行前后应满足数据库约束和业务不变量。数据库可以强制主键、唯一键、外键和检查约束;“账户总额不变”等跨行业务规则还需要应用正确读取、加锁和更新。ACID 的一致性不能替代业务设计。
2.3 隔离性 Isolation
并发事务的中间状态按照隔离级别受到限制。隔离越强,允许观察到的并发结果越少,通常也会增加冲突、等待或回滚概率。
2.4 持久性 Durability
数据库确认提交后,即使进程或机器随后故障,已提交结果仍应按系统承诺恢复。InnoDB 依赖 redo log、刷盘策略和恢复流程;复制副本与备份解决的是更大范围故障,不能仅凭“已经 COMMIT”推断跨机器零丢失。
3. 并发读写会产生多种异常
- 脏读:读取了另一个事务尚未提交的修改。
- 不可重复读:同一事务再次读取同一行时,看到其他事务已提交的新值。
- 幻读:同一事务再次执行范围条件时,结果集合出现新增或删除的行。
- 丢失更新:多个事务基于旧值计算并写回,后一次写入覆盖前一次结果。
- 写偏差:两个事务读取共同约束的不同数据,再分别更新,单行都合法但组合后破坏业务不变量。
前三个名称常用于介绍 SQL 标准隔离级别,但仅靠这三项不能完整判断并发正确性。设计时要写出事务的读集合、写集合和业务约束,再检查特定数据库在目标隔离级别下允许什么执行结果。
4. 四种隔离级别限制不同可见性
4.1 Read Uncommitted
读未提交允许事务读取其他事务尚未提交的修改,因此可能出现脏读。实际系统很少需要这种隔离级别。
4.2 Read Committed
读已提交只读取已经提交的版本。同一事务中的两条普通查询通常会各自建立可见性视图,因此可能看到期间刚提交的新值。PostgreSQL 默认使用 Read Committed。
4.3 Repeatable Read
可重复读要求事务中的重复读取保持稳定。InnoDB 默认配置为 Repeatable Read,普通一致性读通常复用事务级 Read View;锁定读与写操作还会使用当前版本和范围锁,行为不能只用“事务开始时拍快照”概括。
4.4 Serializable
串行化要求并发结果等价于某种串行执行顺序。实现可以通过锁、序列化冲突检测或两者结合完成,并不一定真的让所有事务一次只运行一个。代价通常是更多等待、死锁或序列化失败,应用必须能够处理回滚和重试。
SQL 标准给出隔离语义,数据库采用 MVCC、锁和冲突检测实现。MySQL 与 PostgreSQL 对 Repeatable Read、Serializable 和只读快照的具体行为并不完全一致,结论必须限定数据库、版本、语句类型和是否显式加锁。
5. InnoDB 同时使用 MVCC 与锁
普通 SELECT 通常通过 MVCC 读取符合 Read View 的已提交版本,不会为了读取每行都加共享锁。SELECT ... FOR UPDATE、UPDATE 和 DELETE 读取当前版本并对访问到的索引记录或范围加锁。
在 Repeatable Read 下,InnoDB 的 next-key lock 可以保护索引记录与相邻间隙,减少其他事务在锁定范围内插入。实际加锁范围取决于索引、查询条件和执行计划;缺少合适索引时,扫描范围扩大也会让锁影响更多记录。
MVCC 不能自动避免所有丢失更新或写偏差。计数更新优先使用原子表达式与条件更新,高风险不变量可以使用锁定读、唯一约束或更强隔离级别,并处理死锁与重试。
6. 事务边界要尽量短且完整
可靠的事务通常遵循以下原则:
- 在进入事务前完成不依赖数据库快照的参数校验。
- 事务内只保留维护不变量所需的查询与更新。
- 按稳定顺序访问热点记录,缩短持锁时间。
- 不在持锁期间等待用户输入或执行不受控的远程调用。
- 使用唯一约束、版本号或条件更新保护并发结果。
- 对死锁和序列化失败进行有界重试,并保证业务操作幂等。
- 通过 outbox 等模式把事务结果可靠地交给异步消息流程。
事务过长会保留旧版本、占用连接和锁,增加回滚成本。拆分边界时也不能破坏原本必须共同成立的业务不变量。
7. 常见问题
7.1 可重复读能解决所有并发问题吗
不能。它主要限制重复读取的可见性,具体实现可能继续阻止部分幻读,但基于多个条件的业务不变量仍可能出现写偏差。应根据实际读写集合设计锁、约束或串行化策略。
7.2 方法加了 @Transactional 就能覆盖远程调用吗
不能。本地数据库事务管理数据库连接上的提交与回滚,无法让 HTTP 服务、消息系统或另一个独立数据库原子提交。跨资源流程需要幂等、outbox、Saga、对账或专门的分布式事务协议。
8. 面试题
8.1 ACID 与四种隔离级别分别解决什么问题
出现公司:美团
考察重点
- 原子性、一致性、隔离性和持久性。
- 脏读、不可重复读、幻读与其他并发异常。
- MVCC、锁、事务边界和数据库实现差异。
相关内容:第 1 节“事务保护一组相关状态变化”至第 6 节“事务边界要尽量短且完整”。
参考回答
原子性保证一组修改共同提交或回滚;一致性要求提交前后满足数据库约束和业务不变量;隔离性限制并发事务能够观察到的中间结果;持久性保证数据库确认提交后可以从故障中恢复。数据库机制提供边界,业务代码仍要正确表达约束。
Read Uncommitted 允许脏读,Read Committed 每条语句只看已提交数据,Repeatable Read 让事务内重复读取保持稳定,Serializable 要求结果等价于串行执行。名称相同的级别在不同数据库中实现细节可能不同。InnoDB 会结合 MVCC 与索引锁,应用还要处理丢失更新、写偏差、死锁和重试,并保持事务短小,避免在事务内执行不受控远程调用。