应根据业务是否允许重试、冲突频率及能否接受锁等待来选择:乐观锁适用于读多写少、冲突低的场景(如点赞),悲观锁适用于写密集、强一致场景(如库存扣减);选错会导致超时、死锁或重试失败。

MySQL 事务里怎么选乐观锁还是悲观锁
没有“该用哪个”的标准答案,只看业务是否允许重试、数据冲突频率高低、以及能否接受锁等待。乐观锁适合读多写少、冲突概率低的场景(比如文章点赞数更新);悲观锁适合写密集、必须强一致的场景(比如库存扣减)。选错会直接导致超时、死锁或大量重试失败。
用 SELECT ... FOR UPDATE 实现悲观锁要注意什么
它会在匹配行上加排他锁(X 锁),但具体锁住哪些行,取决于 WHERE 条件是否命中索引:
- WHERE 条件走主键或唯一索引 → 只锁命中行
- WHERE 条件走普通索引或全表扫描 → 可能锁住间隙(Gap Lock)甚至整个范围,引发不必要的锁等待
- 没加
ORDER BY或LIMIT时,InnoDB 可能锁住所有扫描过的记录,哪怕最终只更新一行 - 事务不提交,锁就一直持有——忘记
COMMIT或ROLLBACK是最常见的死锁/阻塞源头
START TRANSACTION; SELECT stock FROM products WHERE id = 123 FOR UPDATE; -- 此时其他事务对 id=123 的 UPDATE/SELECT FOR UPDATE 会被阻塞 UPDATE products SET stock = stock - 1 WHERE id = 123; COMMIT;
乐观锁靠 version 字段实现时的典型陷阱
本质是“先查后判再更新”,靠数据库原子性保证并发安全,但容易在应用层漏掉校验逻辑:
- UPDATE 语句必须包含
WHERE version = ?,且 WHERE 中不能只依赖业务字段(如WHERE status = 'pending'),否则可能覆盖他人修改 - 执行
UPDATE后必须检查ROW_COUNT()(MySQL)或影响行数是否为 1,为 0 就说明版本已变,需重试或报错 - 如果业务逻辑中有多次 UPDATE(比如先改状态再改时间),每次都要带 version 判断,否则中间状态可能被覆盖
- 不要把
version设为TIMESTAMP类型——毫秒级并发下可能重复,推荐用INT或BIGINT自增
UPDATE orders SET status = 'shipped', version = version + 1 WHERE id = 456 AND version = 12;
什么时候两种锁都解决不了问题
当业务需要跨多张表、多个服务、或涉及外部系统(比如调用支付接口后再扣库存),单靠 MySQL 的锁机制无法保证整体一致性。这时候乐观锁和悲观锁都只是局部手段:
- 分布式事务(如 Seata、XA)开销大,且 MySQL XA 对高并发不友好
- 真正可靠的做法是引入补偿事务(Saga)、幂等设计、或用消息队列解耦+最终一致性
- 最容易被忽略的一点:锁只管“数据库写”,不管“业务含义”。比如两个事务都成功扣了库存,但其中一个后续因地址校验失败要回滚——这时库存已经错了,锁本身不负责业务回滚逻辑










