- 事务对语句进行分组,以便它们全部一起成功或全部一起失败。
COMMIT使更改永久生效,ROLLBACK取消更改。- ACID代表原子性、一致性、隔离性和持久性。
- 更强的隔离可以防止更多异常,但它会降低并发性,因此请慎重选择。
将 300 卢比从 Asha 转移到 Ben 需要两次更新:从一个帐户中减去并添加到另一个帐户中。如果他们之间的系统出现故障,金钱就会消失。 SQL 事务 的存在就是为了使这成为不可能,通过将这两个步骤视为一个单元。
在本指南中,我们将在一个小帐户表上运行提交、回滚和失败。我们在 SQLite 中运行这些示例,显示的余额来自该运行。跨数据库的基本命令相似,但锁定和隔离行为差异很大,因此我们指出在哪里查看您自己的数据库文档。
什么是交易?
事务是数据库将其视为单个逻辑操作的一系列一个或多个 SQL 语句。这是我们的桌子:
CREATE TABLE accounts (
id INT PRIMARY KEY,
owner TEXT,
balance INT CHECK (balance >= 0)
);
INSERT INTO accounts VALUES (1, 'Asha', 1000), (2, 'Ben', 200);
CHECK 约束是余额永远不能低于零的规则,我们稍后将使用它。请注意,开始事务的语法有所不同:SQLite 和 PostgreSQL 接受 BEGIN,MySQL 也有 START TRANSACTION,而 SQL Server 使用 BEGIN TRANSACTION。
在显式事务之外,大多数数据库使用 自动提交,其中每个语句在完成后立即保存。 Oracle 是一个值得注意的例外,因为那里的会话一直保留在事务中,直到您提交为止。
COMMIT:使其永久化
BEGIN;
UPDATE accounts SET balance = balance - 300 WHERE id = 1;
UPDATE accounts SET balance = balance + 300 WHERE id = 2;
COMMIT;
-- Asha | 700
-- Ben | 500
在 COMMIT 运行之前,这些更改都是临时的。一旦成功返回,数据库保证它们被保存。两个账户的总金额保持在 1200,这就是本次活动的重点。
ROLLBACK:取消一切
ROLLBACK 撤消自事务开始以来的所有操作:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SELECT owner, balance FROM accounts;
-- Asha | 600 (visible inside this transaction)
-- Ben | 500
ROLLBACK;
SELECT owner, balance FROM accounts;
-- Asha | 700 (back to where it was)
-- Ben | 500
如果在提交之前连接断开或服务器崩溃,回滚也会自动发生,因此半完成的事务将无法生存。
当语句失败时
这是一个更现实的案例。 Asha 尝试发送 5000,但该帐户不持有该金额。 Ben 的贷方首先运行,然后借方打破了 CHECK 规则:
BEGIN;
UPDATE accounts SET balance = balance + 5000 WHERE id = 2;
UPDATE accounts SET balance = balance - 5000 WHERE id = 1;
-- error: CHECK constraint failed: balance >= 0
ROLLBACK;
-- Asha | 700
-- Ben | 500
如果没有这笔交易,本就会保留这 5000 美元,账簿就会出错。请注意一个重要细节:失败的语句并不总是会自行取消事务。例如,在 SQLite 和 MySQL 中,较早的语句可能保持挂起状态,因此您的代码必须发出回滚。相反,PostgreSQL 将整个事务标记为已中止,并拒绝进一步的语句,直到您回滚。
在应用程序代码中,始终将事务与错误处理配对。这是 Python 中的想法,我们的 Python 异常处理 指南解释了语法:
try:
conn.execute("BEGIN")
conn.execute(debit_sql, values)
conn.execute(credit_sql, values)
conn.execute("COMMIT")
except Exception:
conn.execute("ROLLBACK")
raise
保存点
保存点标记事务内的位置,因此您只能撤消部分工作:
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1;
SAVEPOINT bonus;
UPDATE accounts SET balance = balance + 999 WHERE id = 2;
ROLLBACK TO bonus;
UPDATE accounts SET balance = balance + 50 WHERE id = 2;
COMMIT;
-- Asha | 650
-- Ben | 550
999 奖金被撤销,而其余交易继续进行并已提交。这对于长流程中的可选步骤很有用。
ACID 属性解释
| 财产 | 承诺 | 在我们的转会中 |
|---|---|---|
| 原子性 | 全有或全无 | 两个更新都会发生,或者都不发生 |
| 一致性 | 之后规则仍然有效 | 任何平衡都不能打破CHECK规则 |
| 隔离 | 并发工作互不干扰 | 其他人看不到半完成的传输 |
| 耐用性 | 提交的数据在失败后仍然存在 | COMMIT 没有任何损失后停电 |
一致性常常被误解。数据库仅强制执行您声明的规则,例如约束和外键。它不会阻止您的代码转移错误的金额。持久性通常通过预写日志来实现,其中在确认提交之前将更改安全地记录在磁盘上。
并发事务会发生什么?
我们打开了到一个 SQLite 文件的两个连接。第一个启动了一个事务并将一行从 1 更新为 2,但尚未提交。这是第二个连接看到的内容:
Connection B reads: 1 -- no uncommitted data
Connection B writes: database is locked -- it has to wait
(A commits)
Connection B reads: 2 -- now it sees the change
SQLite 一次只允许一名写入者,这比服务器数据库更简单,在服务器数据库中,许多事务可以同时写入不同的行。但原理是一样的:正常设置下其他会话不应该看到未提交的更改,冲突的写入必须等待。在我们的测试中,我们将第二个连接设置为立即放弃而不是等待,因此错误立即出现。
读取异常
当事务重叠时,可能会出现某些问题。 SQL 标准根据事务可能观察到的内容定义了这些:
| 异常 | 会发生什么 |
|---|---|
| 脏读 | 您读取了另一个事务尚未提交的更改,并且稍后可能会回滚 |
| 不可重复读取 | 您在一个事务中读取一行两次并获得不同的值,因为有人在中间提交了 |
| 幻读 | 您运行相同的查询两次并获得额外或缺失的行,因为有人插入或删除了一些行 |
另一个问题是丢失更新。两个会话读取余额为 100,每个会话添加 10,每个会话写入 110。当答案应该是 120 时,一次更新实际上已经消失。
SQL 隔离级别
隔离级别让您可以用严格性换取并发性。以下是 SQL 标准在每个级别允许的内容:
| 级别 | 脏读 | 不可重复读取 | 幻读 |
|---|---|---|---|
| 未提交的读 | 可能 | 可能 | 可能 |
| 读已提交 | 预防 | 可能 | 可能 |
| 可重复读取 | 预防 | 预防 | 可能 |
| 可串行化 | 预防 | 预防 | 预防 |
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
确切的语法和行为取决于您的数据库。默认值也不同:PostgreSQL 和 SQL Server 使用已提交读,而 MySQL 的 InnoDB 使用可重复读。有些数据库提供比标准要求更强的保证,或者将一个级别视为另一级别。例如,在 PostgreSQL 中,未提交读的行为类似于已提交读。在依赖特定行为之前,请务必阅读数据库的文档。
提示: 对于丢失的更新,请在单个语句中进行更改,如 UPDATE accounts SET balance = balance + 10 中所示。根据先前的读取在应用程序中进行计算会打开差距。
僵局
当两个事务各自持有另一个事务所需的锁时,就会发生死锁。事务 A 锁定第 1 行并想要第 2 行,而事务 B 锁定第 2 行并想要第 1 行。两者都无法继续。
PostgreSQL、MySQL 和 SQL Server 等数据库会检测到这一情况并取消一个错误事务。这是正常行为,您的应用程序应该为此做好准备。为了减少死锁,请始终以一致的顺序更新行,例如首先更新最低帐户 ID,保持事务简短,并在选择一个受害者时重试。
最佳实践
- 保持事务简短。长事务持有锁并阻止其他用户。
- 永远不要在事务中等待用户或缓慢的 API。 在开始之前或之后完成这项工作。
- 始终处理失败。 每个路径都应以提交或回滚结束。
- 添加约束。它们为数据库提供了要强制执行的真正规则。良好的表格设计也有帮助。
- 使用良好的索引。更快的查找意味着锁定的持有时间更短。
- 重试时要小心。 发生死锁时重试,但确保一个操作不能应用两次。
- 有意测试并发性。 其中许多错误仅在负载下出现,因此仅靠基本的集成测试可能无法揭示它们。
最后的话
事务将几个有风险的步骤转化为一个安全的操作,ACID 描述了使这一过程值得信赖的保证。每当一项业务操作涉及多个行或表时,问问自己如果它中途停止会发生什么。如果答案让您担心,请将其包装在交易中。要进行练习,请在本地数据库中打开两个会话并尝试自己产生异常。
常见问题
SQL中什么是事务?
事务是一组被视为一个单元的 SQL 语句。要么全部用 COMMIT 保存,要么全部用 ROLLBACK 撤消。
数据库中的 ACID 属性是什么?
ACID 代表原子性、一致性、隔离性和持久性。它们共同确保交易要么全有,要么全无,保持规则完整,不干扰并在失败中幸存。
COMMIT 和 ROLLBACK 有什么区别?
COMMIT 永久保存事务中所做的所有更改。 ROLLBACK 取消它们并将数据返回到事务开始之前的状态。
什么是脏读?
脏读是指一个事务读取另一事务尚未提交的更改。如果其他事务回滚,则读取的数据永远不会真正存在。
我应该使用哪种隔离级别?
从数据库的默认值开始,这通常没问题。对不可接受异常的关键逻辑使用可序列化,并准备好重试失败的事务。
继续学习
需要测试、WordPress 或 SEO 方面的帮助?
我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。
更多指南 测试、人工智能和现代开发
喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。
人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它
人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。
阅读文章 →DevOps环境变量解释:.env 文件、秘密和配置正确完成
使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。
阅读文章 →软件开发API 速率限制解释:算法、标头和处理 429 错误
API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。
阅读文章 →测试自动化Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误
使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。
阅读文章 →软件测试代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义
用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件测试用户验收测试 (UAT):流程、清单和真实示例
用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。
阅读文章 →数据库与 SQLSQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱
通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。
阅读文章 →数据库与 SQLSQL CASE WHEN 语句:语法、实例和常见错误
SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。
阅读文章 →软件开发什么是智威汤逊?通过工作示例解释 JSON Web 令牌
什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。
阅读文章 →软件开发JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例
JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。
阅读文章 →软件开发Python 生成器和 Yield 解释:通过真实示例节省内存
Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。
阅读文章 →人工智能什么是嵌入?向量和余弦相似度简单解释
什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。
阅读文章 →DevOps蓝绿部署与金丝雀部署:差异、权衡以及如何选择
蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。
阅读文章 →测试自动化不稳定的测试:造成这些问题的原因以及如何彻底解决它们
我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。
阅读文章 →软件测试负载测试与压力测试:差异、指标和 k6 示例
负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。
阅读文章 →软件测试测试用例与测试场景:主要区别、示例以及何时使用它们
测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。
阅读文章 →数据库与 SQLSQL 查询优化:读取执行计划并修复慢速查询(带计时)
对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。
阅读文章 →数据库与 SQLSQL UNION 与 UNION ALL:差异、示例和性能测试
SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。
阅读文章 →软件开发JavaScript Fetch API 教程:GET、POST、错误处理和超时
针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。
阅读文章 →软件开发Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例
使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。
阅读文章 →软件开发Python 类和对象:带有实际示例的 OOP 教程
用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。
阅读文章 →人工智能什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释
什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。
阅读文章 →DevOps适合初学者的 Linux 命令:带有示例的基本终端指南
适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。
阅读文章 →测试自动化Selenium 与 Playwright:主要区别、代码比较以及选择
Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。
阅读文章 →软件测试等价划分:通过实例解释测试设计技术
等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。
阅读文章 →软件测试软件测试中的严重性与优先级:差异、矩阵和实例
软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。
阅读文章 →数据库与 SQLSQL 子查询教程:类型、相关子查询和 EXISTS 以及示例
SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。
阅读文章 →数据库与 SQLSQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别
SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →