高并发场景中,乐观锁和悲观锁哪个更适合?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
概念辨析:面试官不仅仅是想知道你知不知道这两种锁的名字,更是想确认你能不能讲清楚它们的底层思想差异——一个 "先锁后操作",一个 "先操作后验证"。
-
场景选型能力:核心中的核心。题干里的 "高并发" 是个伪命题,高并发读和高并发写完全是两码事,写冲突概率高和低又是两码事。能不能拆开讲,决定了这道题的分数档。
-
实践深度:是否熟悉 CAS、版本号、ABA、
SELECT ... FOR UPDATE、synchronized这些具体落地手段,有没有踩过坑。 -
工程化思维:生产环境很少单一锁方案解决一切,能不能给出 "悲观锁 + 拆分"、"Redis 预扣 + 数据库兜底" 这种混合方案,是区分高级和资深的分水岭。
核心答案
没有绝对谁更适合,关键看两个维度:写冲突概率 + 读写比例。
| 场景特征 | 推荐策略 | 典型业务 |
|---|---|---|
| 读多写少,写冲突概率低 | 乐观锁 | 配置更新、订单状态流转 |
| 写多,冲突概率高 | 悲观锁 | 秒杀扣库存、抢红包 |
| 超高并发写,单行热点 | 悲观锁 + 拆分(分桶/分段) | 爆款秒杀 |
| 读多写极少 | 无锁 + 缓存 | 商品详情页 |
记住一句话:写少冲撞低用乐观,写多冲撞高用悲观,热点写还得靠拆分。
深度解析
一、两种锁的本质区别
先把思想讲清楚,不然后面的选型全是空中楼阁。
悲观锁:操作前先锁资源,独占到底。它假定 "一定会有人跟我抢",所以干脆把门关上,谁也别想进来。
乐观锁:操作时不加锁,提交的时候再验证 "这段期间有没有人动过我的数据"。它假定 "大家一般不会冲突",先干活,干完再说。
两者的流程对比:
上图把两种锁的执行路径并排放了。看左边的悲观锁,整个流程被一把锁从头包到尾,期间其他线程只能干等;右边的乐观锁不加锁,但在最后一步做版本校验,没冲突就提交,有冲突就重试或者直接返回失败。
关键差异就两点:
- 悲观锁的代价是 "等待":其他线程被阻塞,上下文切换、锁竞争开销大
- 乐观锁的代价是 "重试":冲突一多,CPU 全花在重试和自旋上
二、"高并发" 要先拆开看
很多人栽在这一步,觉得 "高并发" 就是一道题,其实它至少要拆成两个维度。
维度一:读写比例
- 高并发读、低并发写:乐观锁完胜,因为读不阻塞、不加锁、不互相影响
- 高并发写:要看维度二
维度二:写冲突概率
这是真正决定锁选型的核心。同样是 "高并发写":
- 100 个请求同时改同一行库存 → 冲突概率接近 100%,乐观锁几乎每次都要重试,反而比悲观锁还慢
- 100 个请求改 100 个不同的账户 → 冲突概率极低,乐观锁的代价就微乎其微
所以选型的本质是:冲突高 → 悲观锁(一次锁住,避免无效重试);冲突低 → 乐观锁(避免锁开销)。
三、乐观锁的两种实现
方式一:CAS(Compare And Swap)
Java 里 AtomicInteger、LongAdder 这些原子类底层就是 CAS。核心是一条 unsafe.compareAndSwapInt 指令,CPU 层面的原子操作。
AtomicInteger stock = new AtomicInteger(100);
// CAS 扣减库存
public boolean deduct() {
int current;
int next;
do {
current = stock.get();
if (current <= 0) {
return false; // 库存不足
}
next = current - 1;
} while (!stock.compareAndSet(current, next)); // 自旋重试
return true;
}
这段代码的关键在 compareAndSet:它会比较当前值是不是还等于 current,是的话就更新为 next,不是就返回 false,然后外层 do-while 自旋重试。
方式二:版本号机制
数据库场景最常用,加一个 version 字段:
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = ? AND version = ?;
执行后看影响行数,是 1 就成功,是 0 说明版本号变了,有人抢先改过,业务层重试或返回失败。
四、悲观锁的几种实现
应用层:synchronized、ReentrantLock,适合单机场景。
数据库层:SELECT ... FOR UPDATE,行级排他锁,事务结束才释放。
START TRANSACTION;
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
-- 业务计算
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;
FOR UPDATE 这一行特别关键,它会把查询的行加上排他锁,其他事务想改这行只能等当前事务提交。这里有个大坑:WHERE 条件必须走索引(主键、唯一索引、普通索引都行),一旦走不到索引,行锁会退化为表锁,整张表都被锁住,并发直接崩。而且不同隔离级别下,普通索引还可能加上间隙锁,进一步扩大锁范围。
五、秒杀扣库存的实战选型
这块我专门拎出来讲,因为面试官最爱追这个。
秒杀是典型的高并发写、冲突概率极高的场景。如果直接用乐观锁 CAS 扣数据库库存,会发生什么?1000 个请求同时抢 1 件商品,999 个会失败重试,数据库 CPU 瞬间飙满,这就是经典的 "库存热点" 问题。
正确做法是分层:
这个图说明的是:把高并发挡在数据库前面。Redis 用 Lua 脚本做原子预扣,瞬间拦截掉绝大部分请求;通过的消息进 MQ 异步落库,数据库的写压力就被削峰填谷了;最后数据库层面再用版本号乐观锁做最终一致性兜底,因为到了这一步冲突已经很少。
如果是更极端的爆款,光 Redis 还不够,还得做库存分桶:把 1000 件库存拆成 10 份,每份 100 件存到不同 key,请求按用户哈希路由到不同桶,冲突概率直接降一个数量级。
六、乐观锁的两个大坑
坑一:ABA 问题
线程 1 读到值 A,线程 2 把 A 改成 B 又改回 A,线程 1 的 CAS 还是会成功,因为它眼里值没变。但中间其实发生过修改。
解决办法:用版本号或者 AtomicStampedReference,每次修改都让版本号递增,哪怕值变回原样,版本号也对不上。
坑二:自旋开销
冲突高的时候,CAS 一直失败一直自旋,CPU 直接打满。所以 CAS 只适合冲突少的场景,这也是为什么秒杀这种热点场景不能用纯 CAS。
面试高频追问
- 追问一:CAS 是怎么保证原子性的?
底层是一条 CPU 指令,比如 x86 的 cmpxchg(多核要加 lock 前缀),硬件层面保证 "比较 + 交换" 是不可分割的。Java 通过 unsafe 包调用 native 方法触达这条指令。
- 追问二:
synchronized和ReentrantLock该怎么选?
简单场景用 synchronized(JVM 维护、自动释放、JDK 6 之后性能优化得不错);需要可中断、可超时、公平锁、多条件变量等高级功能用 ReentrantLock。
- 追问三:乐观锁失败了怎么办?
要么业务层重试(要限制重试次数,防止雪崩),要么直接返回失败让用户重新提交。秒杀这种场景一般是直接返回 "请重试",不让无限自旋。
- 追问四:高并发扣库存还有什么其他方案?
除了上面说的 Redis 预扣 + 库存分桶,还有:数据库层把单行库存拆成多行(分桶表)、用消息队列串行化、用 Lua + Redis Cluster 横向扩展。
常见面试变体
- "CAS 的原理是什么?ABA 问题怎么解决?"
- "数据库的乐观锁怎么实现?"
- "
SELECT ... FOR UPDATE会锁表还是锁行?" - "秒杀系统怎么防止超卖?"
记忆口诀
选型三句话:
- 冲突低 → 乐观锁(CAS / 版本号)
- 冲突高 → 悲观锁(
synchronized/FOR UPDATE) - 热点写 → 悲观 + 拆分(分桶 / 分段 / 缓存预扣)
总结
回到原题:高并发场景中乐观锁和悲观锁哪个更适合?答案是 看冲突概率。读多写少、冲突低,乐观锁;写多、冲突高,悲观锁;如果是秒杀这种超高并发热点写,单一锁策略救不了你,得配合 Redis 预扣、库存分桶、MQ 削峰这些手段一起上。能把这个权衡讲清楚,再举一个秒杀扣库存的实战例子,面试官基本会满意。
