第 2 / 22 篇

第 1 章:先确定事务从哪里开始,到哪里结束

一次业务请求可能包含多条 SQL,一条连接也可能先后服务多个请求。数据库不会凭应用的函数名自动识别业务边界。若连接的 autocommit、隔离级别和未结束事务没有被检查,代码看起来在一个方法里,实际却可能分成几次提交。

1.1 显式事务表达一个共同承诺

下面的转账片段表达两个账户余额必须一起变化。示例只在随书实验表中运行,业务系统还应检查账户存在、余额、货币与权限:

START TRANSACTION;
UPDATE accounts SET balance=balance-10 WHERE id=1;
UPDATE accounts SET balance=balance+10 WHERE id=2;
COMMIT;

若程序在第二条 UPDATE 前出错,需要明确回滚,而不是把连接直接交给下一个请求。若异常发生在 COMMIT 的响应阶段,又需要先判断提交结果,后面会专门讨论这个未知窗口。

事务不是一种自动获得业务正确性的包装。把两个错误的 UPDATE 放在一起,只能让错误一起提交;缺少余额条件、业务唯一键或正确的锁顺序,仍然会出问题。

1.2 autocommit 是连接状态

在自动提交模式下,独立语句通常各自构成事务。关闭 autocommit 后,应用必须管理事务结束。驱动也可能有自己的默认值,所以应在实际连接上检查,而不是只查服务器的全局配置。

SELECT @@autocommit, @@transaction_isolation;

随书连接显式设置事务模式和隔离级别。连接池场景还要验证归还连接时是否回滚、是否重置会话设置;一次请求把隔离级别改为 RC,不能让下一位调用者在不知情的情况下继承它。

1.3 保存点只回到事务内部

SAVEPOINT 可以把一个事务内的部分工作撤回,再继续处理。实验先将账户 1 改为 80,建立保存点,将账户 2 改为 120,再回滚到保存点并提交,最终得到 80 与 100。

SAVEPOINT before_second;
-- 执行后续语句
ROLLBACK TO SAVEPOINT before_second;

这不能撤销已经提交的另一个事务,也不能回滚已经发出的邮件。外部系统的副作用需要自己的幂等、补偿或协作协议。

1.4 DDL 不适合混入业务事务

MySQL 中不少 DDL 会造成隐式提交,不能把建表、改表与普通业务更新混在一起,再依赖一个 ROLLBACK 撤销全部工作。本实验把建库建表放在独立初始化阶段,业务测试只处理自己的事务。

数据库迁移还涉及元数据锁、执行耗时、资源占用和版本回退。它需要独立验收,不能因为一段 DDL 在空表上成功就直接用于生产大表。

练习。 应用开始事务后更新账户,调用一个会执行 DDL 的辅助函数,随后捕获异常并 ROLLBACK。为什么不能仅凭最后一条回滚,就认定前面的业务更新没有提交?请为辅助函数写出一个更清楚的责任边界。