🛠️ 项目里事务要注意什么?实战避坑指南
概念都会,一写就翻车。事务边界、大事务、长事务、锁等待、事务里的网络调用……逐个排掉。
先把事务边界管好
🎯 事务要尽可能短
事务越大、时间越长,持有的锁越久、占的资源越多,并发性能越差,还容易超时。
原则:只把真正需要一致性的操作放进事务,其余放外面。
像转账,只有「A扣 + B加」这两步需要在一个事务里。
🔚 尽早提交,别拖
不要在事务里做耗时操作(大量计算、批量循环、远程调用)。
先拿到必要数据 → 尽快完成写操作 → 及时 COMMIT 释放锁。
事务开的越晚、提交的越早越好。
警惕大事务 & 长事务
🐘 大事务:一次改太多行
一次 UPDATE/DELETE 影响几十万行,会:
• 锁大量行/范围
• redo log 巨大
• 回滚慢
对策:分批处理(每批 500-1000 行提交一次)。
⏳ 长事务:运行太久
事务开着一直不提交(比如等外部响应、用户输入)。
会持有连接和锁很久,导致锁等待超时、连接池耗尽。
对策:设置事务超时,尽量避免事务内等待。
锁等待 & 死锁
🔒 锁等待超时
事务 A 锁住某行,事务 B 想改同一行就只能等。
等太久会报 Lock wait timeout exceeded。
对策:缩短事务、检查是不是有大事务/长事务占着锁、优化索引减少锁范围。
💀 死锁
两个事务互相等对方持有的锁,谁也进行不下去。
InnoDB 会自动检测并回滚其中一方(抛 1213 错误)。
对策:统一加锁顺序、让事务尽量短,程序要捕获死锁异常做重试。
事务里的网络调用 & 多数据源
🌐 别在事务里做远程调用
在事务里调外部 API(邮件、支付、第三方)——外部服务慢或失败会拖住事务、持锁很久。
而且外部调用不受数据库事务控制,一旦外部成功、本地回滚,就不一致。
对策:本地事务提交后,再异步发消息/调外部。
🗄️ 多数据源一致性
一个操作要写两个数据库/两张表且要一致,本地事务管不了。
方案:
• 本地消息表 + 异步
• 两阶段提交(X/2PC,实现复杂)
• 消息队列最终一致
不要指望单个 MySQL 事务搞定跨库。
隔离级别与业务匹配
🎚️ 按业务选隔离级别
MySQL 默认可重复读,一般够用。
• 对一致性要求不高、想要更高并发 → 可考虑读已提交
• 极严格一致(金额/库存)→ 保持可重复读,必要时串行化
别盲目改,先理解业务对「脏读/不可重复读/幻读」的容忍度。
📏 业务幂等设计
扣款、发优惠券这类关键写操作,除了事务,还要幂等。
防止:用户重复提交、接口重试导致重复扣款/重复发券。
对策:唯一约束、幂等键、乐观锁/版本号。
唯一约束防重
订单号/幂等键建唯一索引,重复插入直接报错,天然幂等。
版本号/乐观锁
UPDATE ... WHERE version=期望值,更新成功数=0 说明被并发改过。
超时与重试
设置事务超时 + 死锁/锁等待重试机制,别让异常裸奔。
监控长事务
定期查长事务/锁等待(performance_schema),及时止损。
项目里用事务的要点:① 事务边界要短,只放需要一致性的操作,及时提交;② 警惕大事务(分批)和长事务(设超时);③ 留意锁等待(缩短事务、优化索引)和死锁(统一加锁顺序、捕获重试);④ 别在事务里做远程调用,多数据源用消息队列/本地消息表实现最终一致;⑤ 按业务选隔离级别,关键写操作加幂等设计。
我第一会注意事务边界,只把真正需要一致性的操作放进事务,比如转账就 A 扣和 B 加两步,其余放外面,事务开得晚、提交得早;第二警惕大事务和长事务,一次改太多行就分批处理,事务里不等人,设超时;第三关注锁等待和死锁,统一加锁顺序,捕获死锁异常做重试;第四绝不在事务里调外部 API,外部服务不受数据库事务控制,容易不一致,我一般本地事务提交后再异步发消息处理。
我会按这个思路:先看是不是有大事务或长事务占着锁没释放,通过 information_schema 或 performance_schema 查正在运行的事务和锁;再检查是不是并发太集中,多请求同时改同一行;然后看是不是索引没建好导致锁范围扩大,比如本可以只锁几行却锁了全表。定位后再对症:缩短事务、加索引、或者调整并发逻辑。
单个数据库的多表操作直接用本地事务就行,MySQL 事务能保证。但如果要写两个库或者跨系统,本地事务管不了。我一般用本地消息表加异步的方式:本地事务里除了业务写操作,再往一张本地消息表插一条记录,一起提交;后台任务读消息表异步去处理另一个系统,成功就标记完成,失败就重试。这是最终一致性。实在要强一致才考虑两阶段提交,但那个实现复杂、性能也差,一般不轻易用。