Skip to content

MySQL 的乐观锁和悲观锁是什么? ​

  • 标签:后端、MySQL、数据库

🏛 业务架构师 ​

这是一对并发控制思想,我按业务冲突画像来选型:

  • 悲观锁:假设冲突一定会发生,先锁再操作(select ... for update)。适合写多、冲突激烈、错了代价大的场景——账户余额扣减、库存扣减,宁可在锁上排队,也不能超卖超扣;
  • 乐观锁:假设冲突少,不加锁直接改,提交时校验版本号(update ... set version=version+1 where id=? and version=?),失败则重试或放弃。适合读多写少、冲突概率低——用户资料更新、CMS 内容编辑。

业务定价的关键在冲突率与重试成本:乐观锁在高冲突下会大量重试失败,CPU 消耗在无效尝试上,吞吐反而崩(秒杀扣同一行库存就是反例,必须悲观锁或队列串行化);悲观锁在低冲突下白白付出锁开销与死锁风险。

两个实战变体:

  • 库存热点行优化:库存分段(把 100 件拆成 10 个子库存行)或 Redis/Lua 预扣 + 异步落库,本质是把悲观锁的排队从 DB 挪到更便宜的地方;
  • 乐观锁防 ABA:给状态流转加 version,"账户转出又转入、余额相同但世界已变"这类问题靠版本号识别——凡是有状态机的业务(订单状态、审批流),版本号是标配。

一句话:锁策略是按业务冲突率与资损代价定价的,先量化冲突再选型。

🏗 系统架构师 ​

这是两种应对并发的思路,差异在"假设冲突的概率":

  • 悲观锁:假设冲突常发生,操作前先锁住资源。MySQL 里就是 select ... for update(写锁)/ lock in share mode(读锁),或表锁。适合写多、冲突率高、临界区短的场景,比如账户扣款、库存扣减;
  • 乐观锁:假设冲突少,不加锁直接操作,提交时校验版本号:update t set stock = stock - 1, version = version + 1 where id = 1 and version = 5,影响行数为 0 说明冲突,业务重试或放弃。适合读多写少、冲突率低,避免锁等待。

我的选型经验:

  • 秒杀扣库存这类热点行,乐观锁重试风暴反而更糟(大量请求打一圈失败),要么悲观锁排队、要么把热点挪到 Redis/内存用 CAS;
  • 乐观锁的重试要有次数上限和退避,否则高冲突下活锁化;
  • 悲悲锁组合:DB 层 for update 保底,缓存层 CAS 提吞吐。

本质与 JUC 的 synchronized vs AtomicLong 同构:冲突概率决定锁策略,这是并发设计的第一性问题。

💻 全栈开发专家 ​

悲观锁:假设冲突频繁,取数据时就锁住,别人等着。数据库行锁、SELECT ... FOR UPDATE、Java 的 synchronized 都是这个思想。

sql
BEGIN;
SELECT stock FROM sku WHERE id = 1 FOR UPDATE;  -- 锁住这行
UPDATE sku SET stock = stock - 1 WHERE id = 1;
COMMIT;

乐观锁:假设冲突少,不锁,更新时校验版本号,失败则重试。

sql
UPDATE sku SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = #{oldVersion};   -- 返回 0 行说明冲突

选型经验(踩坑总结):

  • 读多写少、冲突率低 → 乐观锁,无锁开销;冲突高时重试风暴会打爆 CPU,反而悲观锁更稳。
  • 冲突激烈的秒杀扣库存 → 两者都不好:悲观锁串行化吞吐低,乐观锁重试率 90%+。我的做法是把冲突前移到 Redis(Lua 原子扣减),DB 层再乐观兜底。
  • 乐观锁有 ABA 问题,纯数值字段无所谓,状态字段建议带 version。
  • 悲观锁注意:必须在事务里用,且 where 条件要走索引,否则锁范围扩大甚至锁全表。