理解数据库事务
当用户在您的平台上下订单时,会同时发生几件事:库存减少、创建支付记录以及保存订单条目。如果服务器在中途崩溃,您可能会遇到客户已被扣款但订单不存在的情况。数据库事务的存在正是为了防止这种部分失败的情况。
本文将介绍什么是数据库事务、ACID 属性如何保证可靠性、事务隔离级别如何控制并发访问,以及现代数据库如何使用 MVCC 来高效实现这一切。
核心要点
- 数据库事务将多个操作组合成一个工作单元——要么全部成功,要么全部回滚。
- ACID 属性(原子性、一致性、隔离性、持久性)定义了事务提供的可靠性保证。
- SQL 定义了四个隔离级别,但在 PostgreSQL、MySQL 和 SQLite 等数据库引擎中,实际行为差异显著。
- MVCC 使现代数据库能够通过维护多个行版本而非依赖重量级锁,高效处理并发读写。
什么是数据库事务?
数据库事务是一系列被视为单个工作单元的操作。要么所有操作都成功并提交更改,要么某个操作失败导致所有内容回滚到之前的状态。
在 SQL 中,一个基本的事务如下所示:
BEGIN;
UPDATE accounts SET balance = balance - 500 WHERE id = 1;
UPDATE accounts SET balance = balance + 500 WHERE id = 2;
COMMIT;
如果在 BEGIN 和 COMMIT 之间发生任何失败,ROLLBACK 会将数据库恢复到事务开始前的状态。大多数数据库还支持 SAVEPOINT,用于在事务内进行部分回滚。
ACID 属性:它们的真正含义
ACID 是使事务可靠的一组保证:
- 原子性(Atomicity) — 所有步骤都成功,或者都不成功。不存在部分更新。
- 一致性(Consistency) — 数据库从一个有效状态转移到另一个有效状态。约束和规则得到强制执行。
- 隔离性(Isolation) — 并发事务之间不会相互干扰。
- 持久性(Durability) — 一旦提交,更改就能在崩溃后保留。这通常通过预写日志(WAL)实现。
ACID 合规性有性能代价。一些系统允许您用严格的保证换取速度,这就是为什么在设计数据层时理解这些属性很重要。
事务隔离级别详解
隔离性是四个 ACID 属性中最微妙的。SQL 定义了四个标准的事务隔离级别,每个级别在一致性和并发性之间允许不同的权衡:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 已防止 | 可能 | 可能 |
| REPEATABLE READ | 已防止 | 已防止 | 可能 |
| SERIALIZABLE | 已防止 | 已防止 | 已防止 |
重要提示: 实际行为因数据库引擎而异。
- PostgreSQL 不实现
READ UNCOMMITTED— 它会静默地将其升级为READ COMMITTED。其默认值是READ COMMITTED,如官方隔离级别指南中所述。 - MySQL (InnoDB) 默认为
REPEATABLE READ。在某些情况下,它使用间隙锁来减少幻读,但行为取决于隔离级别和查询模式,如 InnoDB 隔离文档中所述。 - SQLite 使用单写入者模型,意味着一次只能运行一个写事务。它支持
DEFERRED、IMMEDIATE和EXCLUSIVE事务模式,而非标准隔离级别,如 SQLite 事务文档中所述。
不要假设跨数据库的行为相同。始终检查您特定引擎的文档。
Discover how at OpenReplay.com.
MVCC 如何使隔离性变得实用
大多数现代关系数据库——包括 PostgreSQL 和 MySQL (InnoDB)——通过**多版本并发控制(MVCC)**而非简单锁定来实现隔离性。
使用 MVCC,查询看到的是数据的一致性快照,但该快照是按语句还是按事务获取取决于隔离级别和数据库引擎。写入者创建行的新版本而不是覆盖它们。读取者不会阻塞写入者,写入者也不会阻塞读取者。
这就是为什么 PostgreSQL 可以在不让读取者持续等待锁的情况下处理读密集型工作负载。权衡是旧行版本会累积并且必须清理——在 PostgreSQL 中,这由 VACUUM 进程处理。
MVCC 使 REPEATABLE READ 和快照隔离在大规模应用中变得实用。
不同系统中的事务
并非所有数据库都以相同方式处理事务:
- MySQL (InnoDB) 支持完整的 ACID 事务。较旧的 MyISAM 引擎不支持。
- PostgreSQL 具有强大的 MVCC,并支持具有真正可串行化保证的
SERIALIZABLE隔离。 - SQLite 完全符合 ACID,但会串行化所有写入,使其不适合高并发写入工作负载。
- MongoDB 在 4.0 版本中添加了多文档事务,但它们比传统关系数据库中的开销更大,最好有选择地使用。
使用事务的实用指南
- 保持事务简短。 长时间运行的事务会持有锁或保留旧的 MVCC 快照,这会降低性能。
- 不要不必要地将只读查询包装在事务中 — 特别是在高流量 API 中。
- 显式处理错误。 无论您是编写原始 SQL 还是使用 ORM,始终确保在失败时执行
ROLLBACK。 - 有意识地选择隔离级别。
READ COMMITTED对大多数 Web 应用程序来说是合理的默认值。仅在正确性要求时使用SERIALIZABLE,并测试性能影响。
结论
数据库事务为您提供了一种可靠的方法来保持多个操作之间的数据一致性。理解 ACID 属性告诉您正在使用什么保证。了解隔离级别以及您的特定数据库如何实现 MVCC,告诉您这些保证的实际成本——以及边缘情况在哪里。这些知识将使用事务的开发人员与善用事务的开发人员区分开来。
常见问题
当您的应用程序需要严格的正确性保证时使用 SERIALIZABLE,例如财务计算或库存管理,其中并发事务不得产生冲突结果。对于大多数 Web 应用程序,READ COMMITTED 在一致性和性能之间提供了良好的平衡。始终在您的环境中对 SERIALIZABLE 进行基准测试,因为根据数据库引擎的不同,它可能会降低并发性并导致事务重试。
大多数数据库会自动将每个单独的 SQL 语句包装在隐式事务中,因此单个 INSERT 或 UPDATE 已经是原子的。当您需要将多个语句组合成一个工作单元,确保它们全部成功或全部失败时,显式事务才变得必要。将单个语句包装在 BEGIN 和 COMMIT 中会增加开销而没有实质性好处。
始终在事务逻辑周围使用 try-catch 或等效的错误处理块。如果事务中的任何操作抛出错误,发出 ROLLBACK 以撤消所有更改。许多 ORM 和数据库库提供内置的事务辅助功能,可在异常时自动回滚。避免静默吞没错误,因为这可能会使您的数据处于不一致状态。
传统锁定可能导致读取者和写入者更频繁地相互阻塞,这限制了并发性。MVCC 通过保留每行的多个版本来避免这种情况,使读取者可以访问一致性快照而无需等待写入者。权衡是必须定期清理旧行版本,正如 PostgreSQL 通过其 VACUUM 进程所做的那样。MVCC 通常为读密集型工作负载提供更好的吞吐量。
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.