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 条件要走索引,否则锁范围扩大甚至锁全表。