数据库事务
数据库事务把一组读写放进同一个提交边界。它规定失败恢复、并发可见性和提交后的持久性。本文以 MySQL 8.0 InnoDB 为例。内容包括 ACID、隔离级别、事务边界与提交持久性。
1. 面试核心
先记住这几条,再读正文- ACID 与业务约束。 ACID 描述原子性、一致性、隔离性和持久性。业务不变量通过约束、SQL 与事务边界维护。卡 · ACID
- 隔离级别。 InnoDB 的 RC 为每次一致性读创建快照。RR 复用首次一致性读的快照。Serializable 要求结果等价于串行执行。卡 · ACID
- 原子条件更新。
SET quantity = quantity - 1 WHERE quantity > 0根据当前记录判断库存并扣减。它避免用旧值覆盖写入而造成丢失更新。卡 · 条件更新与丢失更新 - 失败与回滚。
innodb_rollback_on_timeout=OFF时,锁等待超时回滚当前语句。死锁检测选中的受害事务整体回滚。卡 · 自动提交与失败边界 - 提交持久性。
innodb_flush_log_at_trx_commit=1和sync_binlog=1让 redo 与 binlog 在提交成功返回前刷盘。 卡 · 提交持久性与结果确认卡 · 双 1 配置
2. 事务边界
账户转账至少包含扣减付款方余额与增加收款方余额。如果其中一步提交、另一步失败,数据就会违反业务规则。把两步放进一个本地数据库事务,可以让它们共同提交或共同回滚。
下面展示成功路径。每次更新后,应用必须检查影响行数。失败时,应用应执行 ROLLBACK 并停止后续语句:
START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- 应用检查影响行数为 1;否则 ROLLBACK 并结束。
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- 应用检查影响行数为 1;否则 ROLLBACK 并结束。
COMMIT;
应用还要检查两条更新是否各影响一行。数据库约束用于防止非法余额。事务只能覆盖同一数据库能够协调的操作;HTTP 调用、消息发送和另一个数据库不会自动加入这次提交。
3. ACID
3.1 原子性 Atomicity
事务中的修改要么提交,要么在失败时不对外生效。InnoDB 使用 undo log 等机制保存回滚所需信息。应用遇到异常时应结束失败事务,使未提交修改通过回滚撤销。
3.2 一致性 Consistency
事务执行前后应满足数据库约束和业务不变量。数据库可以强制主键、唯一键、外键和检查约束;“账户总额不变”属于跨行业务规则。应用还需正确读取、加锁和更新来维护它。
3.3 隔离性 Isolation
并发事务的中间状态按照隔离级别受到限制。隔离越强,允许观察到的并发结果越少;事务冲突时,锁实现会等待,冲突检测实现会回滚。
3.4 持久性 Durability
数据库确认提交后,已提交结果就应得到持久保护。即使进程或机器随后故障,系统仍应按承诺恢复结果。InnoDB 依赖 redo log、刷盘策略和恢复流程;复制副本与备份用于覆盖机器损毁等更大范围的故障。
4. 并发异常
- 脏读:一个事务读取了另一个事务尚未提交的修改。
- 不可重复读:同一事务再次读取同一行时,看到其他事务已提交的新值。
- 幻读:同一事务再次执行范围条件时,结果集合出现新增或删除的行。
- 丢失更新:多个事务基于旧值计算并写回,后一次写入覆盖前一次结果。
- 写偏差:两个事务读取的数据不同,但受共同约束。它们分别更新后,单行都合法,组合却破坏了业务不变量。
前三个名称常用于介绍 SQL 标准隔离级别。并发正确性还涉及丢失更新和写偏差。设计时要写出事务的读集合、写集合和业务约束。再检查目标数据库在所选隔离级别下允许哪些执行结果。
5. 隔离级别
5.1 Read Uncommitted
读未提交允许事务读取其他事务尚未提交的修改,因此允许脏读。实际系统很少需要这种隔离级别。
5.2 Read Committed
读已提交只读取已经提交的版本。在 InnoDB 中,每次普通一致性读建立新视图。因此,查询可以看到期间刚提交的新值。PostgreSQL 默认使用 Read Committed。
5.3 Repeatable Read
可重复读要求事务中的重复读取保持稳定;InnoDB 默认采用 Repeatable Read。快照时机与混合读写的可见性见 MVCC。
5.4 Serializable
串行化要求并发结果等价于某种串行执行顺序。数据库可通过锁、序列化冲突检测或两者结合实现串行化。只要结果等价于串行执行,事务就可以并发执行。冲突会带来等待、死锁或序列化失败。应用必须能够处理回滚和重试。
SQL 标准给出隔离语义,数据库采用 MVCC、锁和冲突检测实现。MySQL 与 PostgreSQL 的具体行为存在差异。差异涉及 Repeatable Read、Serializable 和只读快照。结论必须明确数据库、版本、语句类型和是否显式加锁。
5.5 默认 RR 与 statement binlog 的历史背景
解释 InnoDB 默认 RR 时,常会提到早期 statement binlog。这涉及 SQL 重放的一致性。MySQL 5.7.7 之前默认 binlog_format=STATEMENT。副本通过重新执行 SQL 还原变更。RR 的 next-key 锁能约束并发插入。这让范围写入和 INSERT ... SELECT 等语句在重放时保持读写范围一致。
例如 A 的范围更新尚未提交,B 向该范围插入并先提交。假设 A 在源库执行时没处理 B 的新行。副本按 B、A 的提交顺序重放 SQL 时,A 却处理了该行。源库与副本就会出现差异。范围锁使 B 的插入等待 A 结束,约束这种执行顺序。
官方文档没有说明默认选择 RR 的原因。上面是常见的解释。MySQL 8.0 默认 binlog_format=ROW,记录行变更。RC/RU 的 InnoDB 写入也要求 row 格式。选择隔离级别仍需依据业务读写语义。
6. InnoDB 并发控制
InnoDB 在 RC/RR 下通过 MVCC 提供普通一致性读。它通过 InnoDB 锁 协调锁定读和写操作。
业务不变量还需要条件更新、唯一约束或锁定读保护。应用还需处理死锁重试。具体选择取决于读写集合和需要共同成立的约束。
7. 事务范围与持续时间
事务范围与持续时间按以下原则设计:
- 在进入事务前完成不依赖数据库快照的参数校验。
- 事务内只保留维护不变量所需的查询与更新。
- 按稳定顺序访问热点记录,缩短持锁时间。
- 不在持锁期间等待用户输入或执行不受控的远程调用。
- 使用唯一约束、版本号或条件更新保护并发结果。
- 对死锁和序列化失败进行有界重试,并保证业务操作幂等。
- 通过 outbox 等模式把事务结果可靠地交给异步消息流程。
事务过长会保留旧版本、占用连接和锁,增加回滚成本。拆分边界时仍需保持原本必须共同成立的业务不变量。
8. 条件更新与丢失更新
假设库存为 10,两个事务都先读取 10,再执行 SET quantity = 9。两次写入后,库存最终只扣减一次。每条 UPDATE 都会取得排他锁。但应用已经算好的旧结果仍会覆盖前一次更新。
把判断与修改放在同一条语句中:
UPDATE inventory
SET quantity = quantity - 1
WHERE sku_id = 100 AND quantity > 0;
两个事务更新同一行时会在写入处协调;后执行者根据可更新的当前记录判断剩余库存。影响行数为 0 表示本次扣减未成功,应用应按扣减失败处理。涉及多个表或跨行约束时,仍需完整事务、锁定读或约束设计。
另一种做法是用 WHERE id = ? AND version = ? 检测版本。应用在更新时同时递增版本号。影响行数为 0 时,应用重新读取并决定是否重试。与把旧值直接覆盖回去相比,它能检测并发修改。MVCC 的实现细节见 MVCC,锁定范围见 InnoDB 锁。
9. 自动提交与失败边界
MySQL 默认启用 autocommit,独立语句各自提交。需要多条 DML 共同成功时,用显式事务或事务管理器绑定同一连接。嵌套调用的提交范围由事务管理器决定;保存点用于事务内的局部回滚,最终提交仍由外层事务完成。
| 情况 | InnoDB 的典型行为 | 应用需要做什么 |
|---|---|---|
| 普通重复键错误 | 回滚当前语句,事务继续有效 | 判断业务是否还能成立,按需回滚整个事务 |
| 检测到死锁 | 选择一个事务整体回滚 | 重开事务,有界重试完整业务单元 |
| 锁等待超时 | innodb_rollback_on_timeout=OFF 时回滚当前语句,ON 时回滚整个事务 | 明确回滚或恢复策略,避免半完成业务继续提交 |
CREATE TABLE、ALTER TABLE 等 DDL | 多数会引发隐式提交 | 不把 DDL 与普通 DML 当成同一个可回滚单元 |
框架会按配置的回滚规则处理异常。命中规则后,框架会把事务标为仅回滚。数据库回滚范围与框架策略需要分别确认。捕获异常后显式终止失败事务,可以避免提交部分业务结果。
10. 提交持久性与结果确认
redo 保存崩溃恢复信息。undo 支持回滚与历史版本。binlog 记录变更,用于复制和按时间点恢复。
10.1 双 1 配置
“双 1”指 innodb_flush_log_at_trx_commit=1 和 sync_binlog=1。前者要求提交时写出并刷盘 redo。后者要求每组 binlog 提交在提交事务前同步到磁盘。组提交可以让多个事务共享一次同步。成功响应前,相应日志仍须完成持久化。
| 配置 | 写出与刷盘时机 | 故障边界 |
|---|---|---|
innodb_flush_log_at_trx_commit=1 | 每次提交写出并刷盘 redo | 依赖操作系统和存储兑现刷盘 |
innodb_flush_log_at_trx_commit=2 | 每次提交写到操作系统缓存,按后台周期刷盘 | 操作系统崩溃或掉电可丢失尚未刷盘的提交 |
innodb_flush_log_at_trx_commit=0 | 后台按周期写出并刷盘 redo | mysqld 崩溃也可丢失仍在日志缓冲区的提交 |
sync_binlog=1 | 每组 binlog 提交同步磁盘 | 与 redo 的提交刷盘配合保护日志一致性 |
sync_binlog=0 | 由操作系统决定 binlog 刷盘时间 | 掉电可丢失尚未落盘的 binlog |
后台周期由 innodb_flush_log_at_timeout 控制,默认 1 秒;调度会影响实际间隔。“双 1”的持久性以可靠存储为前提;异步复制仍有复制延迟。整机或磁盘永久损毁需要副本与备份覆盖。
10.2 提交结果确认
提交阶段断线会让客户端无法确认结果。若数据库已经提交而响应丢失,直接重试会重复执行。因此,扣款或创建订单都可能重复。应用应使用业务唯一标识查询提交结果。再以唯一约束防止重复执行,并决定重试或对账。
11. 常见问题
11.1 可重复读的并发边界
RR 保证重复读取时的可见性稳定;跨行不变量仍需锁、约束或串行化策略。例如两个事务分别更新不同记录。写偏差会破坏它们共同依赖的业务约束。
11.2 本地事务与跨资源操作
本地数据库事务只管理所绑定数据库连接上的提交与回滚。跨资源流程需要幂等、outbox、Saga、对账或专门的分布式事务协议。
12 · 面试题卡片
本页题目与参考回答
12.1 ACID 与四种隔离级别分别解决什么问题
考察重点
- 理解原子性、一致性、隔离性和持久性。
- 理解脏读、不可重复读、幻读与其他并发异常。
- 理解MVCC、锁、事务边界和数据库实现差异。
参考回答
ACID 定义事务提交的保证,隔离级别决定并发事务之间能够观察到哪些结果。
- 原子性让一组修改共同提交或回滚;一致性要求满足数据库约束与业务不变量,业务代码仍需正确表达这些约束。
- 隔离性约束并发可见性;持久性要求确认提交后能够从故障中恢复,具体故障范围还取决于日志与存储配置。
- RU 允许脏读;RC 的一致性读按语句获取已提交快照;InnoDB RR 复用首次一致性读的快照;Serializable 要求结果等价于串行执行。 同名隔离级别在不同数据库中的实现可能不同。
InnoDB 结合 MVCC 与索引锁实现这些保证。应用仍需设计业务约束,并处理丢失更新、写偏差、死锁与重试。
12.2 两个事务都先查询库存再写回,为什么可能少扣一次?
考察重点
- 理解写锁与应用旧值计算的区别。
- 理解条件更新、版本号与影响行数。
相关内容:条件更新与丢失更新。
参考回答
两个事务把同一个旧库存 10 都写成 9,造成丢失更新;写锁将写入排队。
但应用已经计算好的第二个 9 覆盖了前一次结果。使用 quantity = quantity - 1,并把库存约束写进 WHERE。这样就能根据当前记录执行修改。也可带版本号检测冲突。两种方案都必须检查影响行数,多行不变量还要纳入同一事务。
12.3 事务内一条 SQL 报错,前面成功的修改会全部回滚吗?
考察重点
- 理解语句回滚与事务回滚。
- 理解死锁、超时和隐式提交。
相关内容:自动提交与失败边界。
参考回答
回滚范围取决于错误类型。
普通重复键错误只回滚当前语句。innodb_rollback_on_timeout=OFF 下的锁等待超时也只回滚当前语句。死锁受害事务整体回滚。应用应按数据库结果和框架回滚标记结束失败的业务单元;CREATE TABLE、ALTER TABLE 等 DDL 还会隐式提交已有事务。
12.4 数据库返回提交成功就一定不会丢数据吗?提交时断线又该怎么处理?
考察重点
- 理解redo 刷盘、存储保证与复制边界。
- 理解提交响应丢失与幂等核对。
相关内容:提交持久性与结果确认。
参考回答
提交成功后的数据能否恢复,取决于刷盘配置、存储可靠性和故障范围;提交时断线则意味着应用暂时无法确认结果。
innodb_flush_log_at_trx_commit=1 和 sync_binlog=1 配合可靠存储,保护已确认提交的本地日志。异步副本落后或存储永久损毁,还要靠复制与备份策略处理。
断线时可能是数据库已提交、响应却丢失。应用应按业务唯一标识查询结果,再用唯一约束防止重复执行,必要时进入对账流程。
12.5 为什么解释 MySQL 默认 RR 时会提到 statement binlog?
考察重点:statement 重放顺序、范围锁与 row 格式的区别。
相关内容:默认 RR 与 statement binlog 的历史背景。
参考回答
早期默认的 binlog_format=STATEMENT 依赖 SQL 重放。
RR 的 next-key 锁约束并发插入。这有助于保持源库与副本的写入范围一致。因此,解释默认 RR 时常会提到这段兼容历史。MySQL 5.7.7 起默认格式改为 ROW,MySQL 8.0 仍默认 RR。当前的隔离级别选择应依据业务语义。
12.6 “双 1”怎样保护提交持久性,为什么组提交仍可使用它?
考察重点:redo、binlog 刷盘与组提交的同步边界。
相关内容:双 1 配置。
参考回答
innodb_flush_log_at_trx_commit=1 要求提交时刷盘 redo。
sync_binlog=1 要求每组 binlog 提交同步磁盘。二者配合保护成功提交的日志持久性。组提交让多个事务共享同步开销。每个事务仍在相关同步完成后返回成功。实际保证以存储兑现刷盘为前提。
13. 参考资料
- MySQL 8.0:InnoDB Transaction Isolation Levels
- MySQL 8.0:InnoDB Locking
- PostgreSQL:Transaction Isolation
- MySQL 8.0:autocommit、Commit 与 Rollback
- MySQL 8.0:InnoDB Error Handling
- MySQL 8.0:Implicit Commit
- MySQL 8.0:innodb_flush_log_at_trx_commit
- MySQL 5.7:Replication Formats
- MySQL 8.0:Setting The Binary Log Format
- MySQL 8.0:Locks Set by Different SQL Statements
- MySQL 8.0:sync_binlog