📄 MySQL 🟡 进阶 ⏱ 9 分钟

🛠️ 项目里事务要注意什么?实战避坑指南

概念都会,一写就翻车。事务边界、大事务、长事务、锁等待、事务里的网络调用……逐个排掉。

1

先把事务边界管好

🎯 事务要尽可能短

事务越大、时间越长,持有的锁越久、占的资源越多,并发性能越差,还容易超时。

原则:只把真正需要一致性的操作放进事务,其余放外面。

像转账,只有「A扣 + B加」这两步需要在一个事务里。

🔚 尽早提交,别拖

不要在事务里做耗时操作(大量计算、批量循环、远程调用)。

先拿到必要数据 → 尽快完成写操作 → 及时 COMMIT 释放锁。

事务开的越晚、提交的越早越好。

🎬 反面例子:把一堆东西塞进事务
❌ 错误写法
BEGIN → 循环 1000 次逐条 UPDATE → 调外部邮件 API → 等响应 → COMMIT(持锁十几秒,别人全卡住)
✅ 正确写法
先批量准备数据 → 事务里只做几条 UPDATE → 快速 COMMIT → 事务结束后再异步发邮件
2

警惕大事务 & 长事务

🐘 大事务:一次改太多行

一次 UPDATE/DELETE 影响几十万行,会:
• 锁大量行/范围
• redo log 巨大
• 回滚慢

对策:分批处理(每批 500-1000 行提交一次)。

⏳ 长事务:运行太久

事务开着一直不提交(比如等外部响应、用户输入)。

会持有连接和锁很久,导致锁等待超时、连接池耗尽。

对策:设置事务超时,尽量避免事务内等待。

🎬 大事务分批 vs 一次全干
一次全干
UPDATE t SET ... WHERE status=0 (50万行)→ 锁 50 万行,redo 巨大,可能 OOM 或锁冲突
分批
循环:每次 UPDATE ... WHERE status=0 LIMIT 1000 → 提交 → 再下一批,直到处理完
3

锁等待 & 死锁

🔒 锁等待超时

事务 A 锁住某行,事务 B 想改同一行就只能等。

等太久会报 Lock wait timeout exceeded。

对策:缩短事务、检查是不是有大事务/长事务占着锁、优化索引减少锁范围。

💀 死锁

两个事务互相等对方持有的锁,谁也进行不下去。

InnoDB 会自动检测并回滚其中一方(抛 1213 错误)。

对策:统一加锁顺序、让事务尽量短,程序要捕获死锁异常做重试。

🎬 死锁怎么发生
事务 A
UPDATE t SET ... WHERE id=1 → 锁住 id=1 → 想再 UPDATE id=2
事务 B
UPDATE t SET ... WHERE id=2 → 锁住 id=2 → 想再 UPDATE id=1
结果
A 等 B 释放 id=2,B 等 A 释放 id=1 → 死锁!InnoDB 回滚一方
对策
都按 id=1 → id=2 的顺序加锁,就永远不会死锁
4

事务里的网络调用 & 多数据源

🌐 别在事务里做远程调用

在事务里调外部 API(邮件、支付、第三方)——外部服务慢或失败会拖住事务、持锁很久。

而且外部调用不受数据库事务控制,一旦外部成功、本地回滚,就不一致。

对策:本地事务提交后,再异步发消息/调外部。

🗄️ 多数据源一致性

一个操作要写两个数据库/两张表且要一致,本地事务管不了。

方案:
• 本地消息表 + 异步
• 两阶段提交(X/2PC,实现复杂)
• 消息队列最终一致

不要指望单个 MySQL 事务搞定跨库。

🎬 本地事务 + 异步外呼的正确姿势
业务
下单:扣库存 + 建订单(一个本地事务)
本地事务
BEGIN → 扣库存 → 建订单 → 写一条「待发通知」记录 → COMMIT ✅
异步
事务提交后,后台任务/消息队列读到「待发通知」→ 再调邮件/短信 API
好处
事务短、不持锁等外部;外部失败可重试,不影响库存订单的正确性
5

隔离级别与业务匹配

🎚️ 按业务选隔离级别

MySQL 默认可重复读,一般够用。

• 对一致性要求不高、想要更高并发 → 可考虑读已提交
• 极严格一致(金额/库存)→ 保持可重复读,必要时串行化

别盲目改,先理解业务对「脏读/不可重复读/幻读」的容忍度。

📏 业务幂等设计

扣款、发优惠券这类关键写操作,除了事务,还要幂等。

防止:用户重复提交、接口重试导致重复扣款/重复发券。

对策:唯一约束、幂等键、乐观锁/版本号。

🔢
唯一约束防重

订单号/幂等键建唯一索引,重复插入直接报错,天然幂等。

🔖
版本号/乐观锁

UPDATE ... WHERE version=期望值,更新成功数=0 说明被并发改过。

⏱️
超时与重试

设置事务超时 + 死锁/锁等待重试机制,别让异常裸奔。

📊
监控长事务

定期查长事务/锁等待(performance_schema),及时止损。

一句话总结

项目里用事务的要点:① 事务边界要短,只放需要一致性的操作,及时提交;② 警惕大事务(分批)和长事务(设超时);③ 留意锁等待(缩短事务、优化索引)和死锁(统一加锁顺序、捕获重试);④ 别在事务里做远程调用,多数据源用消息队列/本地消息表实现最终一致;⑤ 按业务选隔离级别,关键写操作加幂等设计。

🎤 面试问答 · 口语化回答
这道实战题面试官喜欢结合你项目经历问,想看你有没有真的踩过坑:
面试官你项目里用事务会注意什么?
你

我第一会注意事务边界,只把真正需要一致性的操作放进事务,比如转账就 A 扣和 B 加两步,其余放外面,事务开得晚、提交得早;第二警惕大事务和长事务,一次改太多行就分批处理,事务里不等人,设超时;第三关注锁等待和死锁,统一加锁顺序,捕获死锁异常做重试;第四绝不在事务里调外部 API,外部服务不受数据库事务控制,容易不一致,我一般本地事务提交后再异步发消息处理。

💡 加分点:能说出「事务边界短 + 别做远程调用 + 死锁重试」这几个实战点,比背概念强很多,面试官一听就知道你真做过。
面试官项目里遇到锁等待超时怎么排查?
你

我会按这个思路:先看是不是有大事务或长事务占着锁没释放,通过 information_schema 或 performance_schema 查正在运行的事务和锁;再检查是不是并发太集中,多请求同时改同一行;然后看是不是索引没建好导致锁范围扩大,比如本可以只锁几行却锁了全表。定位后再对症:缩短事务、加索引、或者调整并发逻辑。

💡 加分点:能提到用 information_schema/performance_schema 查锁,说明你真的排查过生产问题,是明显的加分项。
面试官一个操作要改两个表或两个库,怎么保证一致?
你

单个数据库的多表操作直接用本地事务就行,MySQL 事务能保证。但如果要写两个库或者跨系统,本地事务管不了。我一般用本地消息表加异步的方式:本地事务里除了业务写操作,再往一张本地消息表插一条记录,一起提交;后台任务读消息表异步去处理另一个系统,成功就标记完成,失败就重试。这是最终一致性。实在要强一致才考虑两阶段提交,但那个实现复杂、性能也差,一般不轻易用。

💡 加分点:能主动区分「本地事务管多表、最终一致管跨库」,并讲出本地消息表的实现,说明你有分布式场景的实践经验。
📝 读完打卡 · 写下你的收获 读完了?来打卡吧
用一句话写下你从这篇学到的最大收获,检验自己是否真的懂了 👇
⏱ 00:00 🔥0分