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)、幂等设计、或用消息队列解耦+最终一致性 最容易被忽略的一点:锁只管“数据库写”,不管“业务含义”。比如两个事务都成功扣了库存,但其中一个后续因地址校验失败要回滚——这时库存已经错了,锁本身不负责业务回滚逻辑