数据库入门到初级 · 第 17/22 篇。实验口径:MySQL 8.4、InnoDB、utf8mb4。案例:小型商城。
下单不只是往订单表里插一行。至少还要确认商品可售、扣减库存、创建订单、写入明细。如果扣了库存却没建成订单,库存就少了;如果订单建成却没扣库存,又可能卖超。事务把这些必须一起成功的修改放进一个工作单元。
1. 事务的直觉:要么一起完成,要么撤销
START TRANSACTION;
UPDATE products SET stock = stock - 1
WHERE id = 1 AND is_on_sale = 1 AND stock >= 1;
SELECT ROW_COUNT() AS changed_products;
先单独执行上面这段。只有 changed_products=1 才继续下段;否则立即 ROLLBACK 并结束。SQL 脚本不会因为返回 0 行就自动跳到回滚,应用必须检查并做分支。
INSERT INTO orders (user_id, status, total_amount)
VALUES (1, 'pending', 199.00);
SET @new_order_id = LAST_INSERT_ID();
INSERT INTO order_items
(order_id, product_id, product_name, unit_price, quantity)
VALUES (@new_order_id, 1, '机械键盘', 199.00, 1);
SELECT stock FROM products WHERE id = 1;
SELECT * FROM orders WHERE id = @new_order_id;
ROLLBACK;
练习基于原始样例,期间会看到库存从 20 变成 19,并看到新订单;最后回滚后库存恢复,订单和明细也不会保留。价格在这里固定为样例的 199,真正应用应读取并验证当前价格,第 21 篇会给出程序示例。

2. ACID 不必硬背,放进例子理解
| 特性 | 本例中的直观含义 |
|---|---|
| 原子性 Atomicity | 事务内需要完成的修改作为整体提交或撤销 |
| 一致性 Consistency | 合法状态经过事务仍满足已经定义的约束和业务规则 |
| 隔离性 Isolation | 多个事务并发时,按所选隔离级别与锁规则相互影响 |
| 持久性 Durability | 成功提交后的保存由日志、存储和相关配置保障 |
一致性不是数据库替你发现所有业务错误。把订单金额写错,只要没有相应检查,事务也可能成功提交。持久性也不是“任何硬件故障都绝不会丢数据”,实际保障与刷盘配置、存储系统及故障场景有关。
3. 自动提交容易让人误会
SELECT @@autocommit;
许多命令行连接默认开启自动提交,每条普通修改语句单独提交。显式 START TRANSACTION 后,直到 COMMIT 或 ROLLBACK 才结束当前事务。
不同驱动可能设置不同默认值,所以应用不能凭命令行经验猜。尤其使用连接池时,要知道连接归还前是否清理了未完成事务,以及下一次借出的会话有哪些设置。
4. 一条语句失败,不代表整个事务已经撤销
例如第二条插入因为唯一约束失败,前面成功的库存修改可能仍留在当前事务中。应用如果继续提交,就可能把残缺结果保存下来。正确方式是在必要步骤失败后,明确回滚整个业务事务。
有些错误(例如死锁导致事务被选为牺牲者)会让 InnoDB 回滚事务;另一些错误只影响当前语句。不要用一个笼统假设覆盖所有错误,应统一处理异常并保证连接状态清楚。
5. 事务应该短,而且使用同一连接
库存更新、订单和明细写入必须使用同一事务连接。不能在连接 A 扣库存、连接 B 建订单,却以为一次 COMMIT 能提交两边。
事务期间不要等待用户确认、发送耗时邮件或调用不确定延迟的外部接口。数据库事务也不能直接把外部支付平台的扣款一起回滚;跨系统流程需要进一步的幂等、消息和补偿设计。
本例把创建待支付订单理解为预留库存。完整商城还需要超时释放、取消回补和支付状态协调,本篇只演示数据库内的原子修改。
练习与答案
练习:故意让明细数量为 0,再尝试插入,会发生什么? 检查约束会拒绝该明细;随后应主动执行 ROLLBACK,而不是继续提交之前的扣库存结果。
练习:回滚后新订单编号跳过去了,是否说明回滚失败? 不是。自增编号可能不回收,判断是否撤销应查询记录和库存,而不是要求编号连续。
延伸阅读
上一篇:第 16 篇 · 系列目录 · 下一篇:第 18 篇