事务必须在业务逻辑层显式控制,mysql不支持跨表自动一致性,所有多表/多次写入操作须用begin/commit/rollback包裹;嵌套事务不可靠,应使用savepoint实现局部回滚;长事务需避免外部调用,select ... for update须命中索引;分布式场景下本地事务仅保单库acid,最终一致性需业务层通过outbox、幂等、补偿等机制保障。

事务必须在业务逻辑层显式控制,MySQL 本身不支持跨表自动一致性
MySQL 的 START TRANSACTION 只管语句执行期间的锁和回滚边界,它不会感知你的“业务含义”。比如用户下单(插入订单 + 扣库存 + 记日志)看似一个动作,但 MySQL 看到的是三条独立语句——中间任何一条失败,剩下的不会自动撤销,除非你用 ROLLBACK 显式兜底。
常见错误现象:INSERT INTO orders ... 成功了,UPDATE inventory SET stock = stock - 1 WHERE id = ? 却因唯一键冲突失败,结果订单已建、库存没扣,数据错位。
- 所有涉及多个表/多次写入的业务操作,必须用
BEGIN/COMMIT/ROLLBACK包裹,且由应用代码统一判断成败 - 避免在存储过程中硬编码事务逻辑——调试难、监控难、无法和上游服务协同重试
- 如果用 ORM(如 Django ORM、MyBatis),确认它是否真正开启事务(例如 Django 的
@transaction.atomic是必需的,不是默认开启)
嵌套事务不可靠,用保存点(SAVEPOINT)替代
MySQL 不支持真正的嵌套事务,START TRANSACTION 在已有事务内执行,只会警告并忽略。想局部回滚某一步?得靠 SAVEPOINT。
使用场景:批量导入时,某条记录校验失败,只丢弃这条,不影响前面已成功处理的记录。
- 用
SAVEPOINT sp1设立标记,出错时ROLLBACK TO sp1,再继续后续逻辑 -
RELEASE SAVEPOINT sp1可显式清理,但不释放不影响事务本身;重复定义同名保存点会覆盖前一个 - 注意:保存点不能跨连接生效,每个数据库连接维护自己的保存点栈
长事务导致锁等待、主从延迟、连接堆积
业务层事务持续时间越长,MySQL 持有行锁/间隙锁的时间就越久,其他查询卡在 Waiting for table metadata lock 或 Locked 状态是常态。
典型诱因:事务里调外部 HTTP 接口、生成 PDF、读大文件、做复杂计算。
- 把非数据库操作全移出事务块——先查数据、再关事务、最后发通知或写日志
- 用
SELECT ... FOR UPDATE时,务必确保 WHERE 条件命中索引,否则升级为表锁 - 监控
information_schema.INNODB_TRX表,重点关注TRX_STARTED和TRX_STATE,及时发现运行超 5 秒的事务
分布式场景下,本地事务只是基础,别指望它解决最终一致性
单库事务能保 ACID,但一旦涉及微服务、消息队列、第三方支付回调,MySQL 自己的事务就失效了。比如订单服务扣款成功、发 MQ 失败,下游库存服务收不到消息——这已经不是 SQL 能管的事。
这时候本地事务只负责“把当前库的状态改对”,后续补偿、幂等、对账都得靠业务层设计。
- 关键操作后立即写一条
outbox表(含事件类型、payload、status=‘pending’),用定时任务扫描并投递 MQ - 回调接口必须实现幂等:用
INSERT IGNORE INTO order_callbacks (order_id, callback_id) VALUES (?, ?)防重入 - 不要在事务里直接调
curl或send_message()—— 网络抖动会导致整个事务卡死或误回滚
最常被忽略的一点:事务提交成功 ≠ 业务成功。MySQL 返回 OK,只代表这一批 SQL 被持久化了;而用户看到“下单成功”,背后可能还差三次重试、两次核对、一次人工干预。










