乐观锁 vs 悲观锁 知识总结
一、基础定义
乐观锁
- 核心思想 :操作数据时乐观假设其他线程不会同时修改,不主动加锁,仅在更新时校验数据是否被改动。
- 本质:无锁并发控制,通过"版本校验"保证数据一致性。
悲观锁
- 核心思想 :操作数据时悲观假设其他线程一定会修改,操作前先加锁,其他线程需阻塞等待锁释放。
- 本质:独占锁机制,通过"阻塞等待"保证数据一致性。
二、执行流程
乐观锁大体流程
- 多线程(如线程A、B)直接获取共享数据,不加锁执行各自操作。
- 更新数据前,先校验数据是否被其他线程修改。
- 若数据未被修改:直接更新内存中的数据值。
- 若数据已被修改:根据业务需求选择报错 或重试更新。
悲观锁大体流程
- 多线程(如线程A、B)尝试获取同一同步锁。
- 若线程A先获取锁并执行操作,线程B进入阻塞等待状态,直到A释放锁。
- 线程A释放锁后,CPU唤醒等待线程(如线程B),线程B再次尝试获取锁。
- 线程B成功获取锁后,执行自身操作。
三、实现方式
乐观锁的实现
1. CAS 实现
- Java 中
java.util.concurrent.atomic包下的原子变量(如AtomicInteger)采用 CAS 实现乐观锁。 - 核心逻辑:
Compare And Swap(比较并交换),先比较内存值与预期值,一致则更新,不一致则重试。
2. Version 版本号机制(数据库场景常用)
-
数据表新增
version字段,记录数据修改次数,数据更新时version自动 +1。 -
执行流程:
- 查询数据时,同时读取
version值。 - 更新时,携带读取到的
version值,仅当数据库中version与传入值一致时才执行更新,并将version+1。
- 查询数据时,同时读取
-
SQL 示例:
sqlUPDATE user SET name = "zs", version = version + 1 WHERE id = 1 AND version = oldVersion;
悲观锁的实现
1. 数据库层面
-
传统关系型数据库的锁机制:行锁、表锁、读锁、写锁等,均在操作前加锁。
-
SQL 示例(MySQL 行锁):
sqlSELECT * FROM user WHERE id = 1 FOR UPDATE;
2. Java 语言层面
- 独占锁实现:
synchronized关键字、ReentrantLock可重入锁。 - 特点:线程获取锁后,其他线程必须阻塞等待,直到锁被释放。
四、优缺点与适用场景
乐观锁
- 优点:无锁竞争,读操作性能极高,适合高并发读场景。
- 缺点:写冲突频繁时,重试次数多会降低性能;无法解决 ABA 问题(CAS 场景)。
- 适用场景 :读多写少的业务场景(如商品详情查询、统计报表),追求读性能。
悲观锁
- 优点:强一致性保证,写操作数据安全,适合并发写入频繁场景。
- 缺点:锁竞争导致线程阻塞,并发性能低;存在死锁风险。
- 适用场景 :写多读少的业务场景(如库存扣减、订单支付),追求数据正确性。
五、核心对比总结
| 维度 | 乐观锁 | 悲观锁 |
|---|---|---|
| 核心思想 | 假设无冲突,更新时校验 | 假设必冲突,操作前加锁 |
| 加锁时机 | 无锁,更新时校验 | 操作前加锁 |
| 性能表现 | 读性能高,写冲突时性能下降 | 读写性能均受锁竞争影响,并发低 |
| 数据一致性 | 弱一致性,依赖版本校验 | 强一致性,锁独占保证 |
| 典型实现 | CAS、Version 版本号 | 数据库行锁、synchronized、ReentrantLock |
| 适用场景 | 读多写少(如查询、统计) | 写多读少(如库存、支付) |
面试模板
乐观锁 vs 悲观锁 选型指南(面试/开发通用)
核心选型原则
选择的核心逻辑:以"读写比例"和"业务一致性要求"为核心,兼顾性能与数据安全,以下是可直接落地的选型步骤和判断标准。
一、第一步:先判断核心场景(读写比例)
场景1:读多写少(90%读 + 10%写)→ 优先选乐观锁
典型业务场景
- 电商商品详情页查询、新闻资讯浏览、用户信息展示;
- 统计报表、数据看板(仅偶尔更新基础数据);
- 缓存预热、非核心数据的批量查询。
选型理由
- 乐观锁无锁竞争,读操作无需等待,能最大化提升并发读性能;
- 写操作占比低,冲突概率小,少量重试/报错不会影响整体业务。
落地示例
- Java 代码:用
AtomicInteger处理计数器(如商品浏览量统计); - 数据库:用 Version 版本号机制(如用户昵称修改,冲突概率极低)。
场景2:写多读少(90%写 + 10%读)→ 优先选悲观锁
典型业务场景
- 电商秒杀库存扣减、订单状态更新;
- 金融账户余额变动、交易流水写入;
- 秒杀/抢购、限量优惠券发放。
选型理由
- 写操作频繁,乐观锁会因频繁冲突导致大量重试,反而降低性能;
- 悲观锁通过独占锁保证写操作的原子性,避免数据脏写/覆盖,符合强一致性要求。
落地示例
- Java 代码:用
synchronized/ReentrantLock处理库存扣减; - 数据库:用
SELECT ... FOR UPDATE加行锁(如扣减库存前锁定订单行)。
二、第二步:补充判断业务一致性要求
要求1:强一致性(不允许数据不一致)→ 优先悲观锁
核心特征
- 涉及资金、核心交易、合规性要求高的场景;
- 数据错误会导致直接经济损失或合规风险(如转账、支付、税务数据)。
示例
- 银行转账:A账户扣钱、B账户加钱必须原子性完成,绝对不允许中间状态;
- 秒杀库存:必须保证库存扣减准确,不超卖、不少卖。
要求2:弱一致性(允许短暂不一致,最终一致即可)→ 优先乐观锁
核心特征
- 非核心数据、允许少量重试的场景;
- 数据不一致可通过后续补偿/重试修复(如用户积分、商品浏览量)。
示例
- 商品点赞数:短暂的计数偏差可接受,最终同步即可;
- 个人资料修改:即使冲突重试,对用户体验影响极小。
三、第三步:考虑技术成本与性能瓶颈
乐观锁的技术成本&风险
- 优点:实现简单(如 MyBatis-Plus 乐观锁插件一键配置)、无死锁风险;
- 缺点:需处理重试逻辑(如 CAS 自旋),高并发写场景下重试次数过多会导致 CPU 飙升;
- 避坑点 :CAS 存在 ABA 问题,需通过版本号/时间戳解决(如
AtomicStampedReference)。
悲观锁的技术成本&风险
- 优点:逻辑简单,无需处理冲突重试,数据安全;
- 缺点:存在死锁风险(需规范加锁顺序)、高并发下线程阻塞导致性能下降;
- 避坑点 :数据库悲观锁(如
FOR UPDATE)需注意锁粒度,避免锁表(优先行锁)。
四、选型决策表(直接对照)
| 场景特征 | 推荐选型 | 典型落地方式 |
|---|---|---|
| 读多写少 + 弱一致性 | 乐观锁 | MyBatis-Plus 版本号/Atomic 原子类 |
| 写多读少 + 强一致性 | 悲观锁 | synchronized/ReentrantLock/数据库行锁 |
| 核心交易/资金场景 | 悲观锁 | 数据库行锁 + 事务 |
| 非核心数据/统计场景 | 乐观锁 | CAS 原子操作 |
| 高并发读 + 偶尔写 | 乐观锁 | 版本号机制 + 重试逻辑 |
| 低并发写 + 数据安全优先 | 悲观锁 | synchronized 关键字 |
五、面试高频回答模板(精简版)
选择乐观锁或悲观锁,核心看业务场景的读写比例 和数据一致性要求:
- 若场景是读多写少、弱一致性要求(如商品详情、积分统计),优先选乐观锁,无锁竞争能提升读性能,少量写冲突可通过重试解决;
- 若场景是写多读少、强一致性要求(如库存扣减、转账),优先选悲观锁,通过加锁保证数据安全,避免并发写导致的数据错误;
- 技术层面补充:乐观锁需处理 ABA 问题和重试逻辑,悲观锁需注意死锁和锁粒度问题。