☰
悲观锁与乐观锁
2026/10/2 10:45:12 网站建设 项目流程

悲观锁和乐观锁是两种截然不同的并发控制思想

悲观锁假设:冲突必然发生,操作前先加锁以确保独占资源;
乐观锁假设:冲突概率较低,先执行操作,仅在提交时校验数据一致性。
选择依据核心在于业务场景的冲突频率:
写多读少:强一致性要求高的场景用悲观锁;
读多写少:冲突较少的场景用乐观锁。


一、核心定义与思想

1. 悲观锁

  • 核心思想:假设并发冲突一定会发生,因此在访问数据前主动加锁,确保其他线程/事务无法同时修改数据。
  • 行为模式:“先锁后操作”,类似“锁门上厕所”,必须拿到锁才能继续执行。
  • 典型场景:银行转账、库存扣减、秒杀等强一致性要求高、写操作频繁的场景。

2. 乐观锁

  • 核心思想:假设并发冲突很少发生,操作时不加锁,仅在提交更新时检查数据是否被修改过。
  • 行为模式:“先操作后验证”,类似“超市自助结账”,冲突时重试或报错。
  • 典型场景:商品浏览、点赞计数等读多写少、冲突概率低的场景。

3. 两者对比

对比维度悲观锁乐观锁
核心思想假设冲突必然发生,操作前强制加锁以确保独占资源假设冲突很少发生,先执行操作,提交时校验数据一致性
加锁时机读取数据时立即加锁(如SELECT ... FOR UPDATE)不加锁,仅在提交更新时校验(如版本号比对)
实现方式数据库行锁(FOR UPDATE)、synchronized、ReentrantLock版本号机制、CAS 算法、时间戳校验
并发性能较低(锁竞争导致阻塞,高并发下吞吐量下降)较高(无锁设计,冲突少时吞吐量显著提升)
一致性保障强一致性(ACID 事务级别,绝对避免脏写)最终一致性(冲突时需重试,短暂不一致可接受)
失败处理阻塞等待锁释放(可能超时)提交失败需重试或报错(如返回“数据已被修改”提示)
典型风险死锁、锁等待超时、长事务性能瓶颈ABA 问题、高冲突下重试风暴(CPU 消耗激增)
适用场景- 写多读少(如银行转账、库存扣减)-冲突率 >40%-强一致性要求(金融交易)- 读多写少(如点赞计数、配置更新)-冲突率 <20%-可容忍短暂不一致
代码复杂度较低(依赖数据库或 JVM 原生支持)较高(需自行实现重试逻辑、冲突合并策略)

二、实现机制

1. 悲观锁的实现

  • 数据库层面:
    • 通过SELECT ... FOR UPDATE(排他锁)或SELECT ... FOR SHARE(共享锁)显式加锁,需在事务中使用。
    • 必须确保索引命中,否则可能锁全表(如 MySQL 中未走索引的FOR UPDATE会锁整张表)。
  • Java 语言层面:
    • synchronized关键字、ReentrantLock等独占锁机制,线程竞争时会阻塞等待。

数据库层面和Java层面仅需选择一种
FOR UPDATE适用于分布式部署的多服务器
synchronized适用于单服务器

2. 乐观锁的实现

  • 版本号机制:
    • 表中增加version字段,更新时校验版本号是否匹配:
      UPDATEproductsSETstock=stock-1,version=version+1WHEREid=1ANDversion=旧值;
    • 若影响行数为 0,说明数据已被修改,需重试或报错。
  • CAS(Compare and Swap):
    • 通过 CPU 原子指令实现,如 Java 的AtomicInteger:
      AtomicIntegercount=newAtomicInteger(0);count.incrementAndGet();// 底层通过 CAS 重试实现
    • 需注意 ABA 问题(值被修改后又恢复),可通过AtomicStampedReference添加版本戳解决。

三、关键对比

1. 适用场景

  • 悲观锁更适合:
    • 写多读少(如订单支付、库存扣减)。
    • 冲突概率高(>20%)或强一致性要求严格(如金融交易)。
    • 临界区执行时间短(避免长时间锁持有)。
  • 乐观锁更适合:
    • 读多写少(如商品详情页、用户配置更新)。
    • 冲突概率低(<20%)且能容忍短暂不一致。
    • 高并发场景(避免锁竞争导致的性能瓶颈)。

2. 性能与风险

维度悲观锁乐观锁
并发性能低(阻塞等待,吞吐量受限)高(无锁竞争,冲突少时效率更优)
一致性保障强一致性(ACID 事务级别)最终一致性(需处理冲突重试)
失败处理阻塞直至获取锁提交失败需重试或报错
典型风险死锁、锁等待超时ABA 问题、高冲突下重试风暴

3. 悲观锁的陷阱

  • 死锁风险:多资源交叉加锁时易发生(如事务 A 锁资源 X 后申请 Y,事务 B 锁 Y 后申请 X)。
    规避:按固定顺序加锁、设置锁超时时间。
  • 性能瓶颈:未走索引的FOR UPDATE会锁全表(MySQL 中常见问题)。
    规避:确保查询条件命中索引,避免全表扫描。

4. 乐观锁的陷阱

  • ABA 问题:值被修改后恢复原状,导致校验通过但逻辑错误(如库存从 10→5→10)。
    规避:用版本号替代时间戳(自增版本号可追溯修改次数)。
  • 重试风暴:高冲突场景下重试逻辑可能耗尽 CPU 资源。
    规避:限制最大重试次数(如 3 次)、采用指数退避算法调整重试间隔。

四、实际应用建议

1. 选择原则

  • 冲突频率是核心指标:
    • 若冲突率>40%,悲观锁更高效(避免频繁重试消耗 CPU)。
    • 若冲突率<20%,乐观锁性能优势显著(吞吐量可提升 5-10 倍)。
  • 业务一致性要求:
    • 金融级操作必须用悲观锁;非核心数据(如浏览量)可用乐观锁。

2. 混合策略

  • 动态切换:监控冲突率,低冲突时用乐观锁,高冲突时自动降级为悲观锁。
  • 分层设计:
    • 入口层用乐观锁过滤大部分请求。
    • 核心层用悲观锁保障关键事务。

3. 常见误区

  • 乐观锁性能一定更好?
    否。高冲突场景下,乐观锁的重试开销可能超过悲观锁的阻塞成本。
  • synchronized 是纯悲观锁?
    否。JVM 会自适应:初始用轻量级锁(类似乐观策略),竞争激烈时升级为重量级锁。

五、典型示例

1. 库存扣减(悲观锁)

BEGIN;SELECTstockFROMproductsWHEREid=1FORUPDATE;-- 先加锁IFstock>0THENUPDATEproductsSETstock=stock-1WHEREid=1;ENDIF;COMMIT;

2. 库存扣减(乐观锁)

-- 查询时获取版本号SELECTstock,versionFROMproductsWHEREid=1;-- 更新时校验版本UPDATEproductsSETstock=stock-1,version=version+1WHEREid=1ANDversion=旧值;

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询