Skip to content

乐观锁与悲观锁对比及应用场景

核心思想对比:

  1. 悲观锁:

    • 思想: “凡事预则立,不预则废”。它假设冲突(数据被其他事务修改)是大概率事件。因此,在访问或修改数据之前,它会预先获取锁(通常是排他锁),将数据锁定,阻止其他事务访问或修改该数据,直到当前事务完成(提交或回滚)。
    • 态度: 悲观、保守、防御性。
    • 类比: 就像你去图书馆借一本非常热门的书,管理员担心别人在你阅读时抢走它,所以在你开始阅读前就把书锁在阅览室里,只有你还书(事务结束)后,别人才能借。
  2. 乐观锁:

    • 思想: “船到桥头自然直”。它假设冲突是低概率事件。因此,它不会在读取数据时加锁。多个事务可以同时读取同一份数据。只有在事务提交更新时,才会检查该数据自读取以来是否被其他事务修改过。
    • 机制: 通常通过添加一个版本号字段(如 version)或时间戳字段(如 last_updated_at)来实现。读取数据时记录下这个版本号/时间戳。更新数据时,在 WHERE 子句中检查当前数据库中的版本号/时间戳是否与之前读取到的一致。如果一致,则更新成功并递增版本号/更新时间戳;如果不一致,则意味着数据已被修改,更新失败(通常需要回滚事务并重试)。
    • 态度: 乐观、开放、事后检查。
    • 类比: 就像你在线编辑一个共享文档(如 Google Docs)。你可以直接开始编辑,系统不会阻止别人同时编辑。当你点击保存时,系统会比较你编辑的版本和当前服务器版本。如果在你编辑期间没人改动过,保存成功;如果有人改动过,系统会提示你冲突,让你基于最新版本重新编辑(重试)。

实现方式对比:

特性悲观锁乐观锁
加锁时机读取数据时立即加锁(通常是排他锁)读取数据时不加锁
检查时机在加锁时隐式保证数据不会被并发修改提交更新时检查数据版本/时间戳是否被修改
常用实现SELECT ... FOR UPDATE (数据库)数据表增加 version 字段 + UPDATE ... WHERE id=? AND version=?
Java synchronized 关键字Java AtomicStampedReference
Java ReentrantLockCAS (Compare-And-Swap) 操作 (如 Java AtomicInteger)
锁粒度通常锁定行(数据库)或对象(代码)无实际锁,冲突检测基于版本号/时间戳(通常也是行级)
阻塞其他需要该锁的事务会阻塞等待无阻塞,只有在提交时发现冲突才失败/重试

优缺点对比:

特性悲观锁乐观锁
优点强一致性保证: 锁定期间数据绝对安全,不会发生覆盖。高并发性能: 读操作完全不加锁,并发读性能极高;写冲突少时性能极佳。
实现相对简单直观: 符合传统加锁思维。避免死锁: 没有真正的锁持有,不易产生死锁。
减少锁开销: 避免了大量加锁、解锁操作的开销。
缺点性能开销大: 加锁、释放锁本身有开销;阻塞等待会严重降低并发吞吐量。冲突处理复杂: 更新失败后需要业务逻辑处理回滚和重试(可能多次)。
死锁风险: 不当的加锁顺序容易导致死锁。ABA 问题: (CAS 特有) 值从 A->B->A,仅检查值会误认为没变。需版本号/时间戳解决。
降低并发度: 锁定的资源其他事务无法访问,降低了系统整体并发能力。弱一致性: 读到的可能是稍纵即逝的“脏”数据(虽然最终提交时会被检查)。
不适用于高并发读场景: 即使只是读也要加锁,浪费资源。冲突频繁时性能差: 如果写冲突频繁,大量的回滚重试会抵消性能优势。

典型业务使用场景:

  1. 悲观锁适用场景:

    • 写操作非常频繁,冲突概率极高: 例如,银行核心系统的账户余额扣减、火车票/演唱会票的选座锁定(用户选中座位到支付的短暂锁定)。在这些场景下,冲突几乎是必然的,悲观锁可以避免大量无效的重试。
    • 需要绝对保证数据一致性且不允许重试的业务: 例如,金融交易、库存扣减(如果超卖后果严重且不能简单重试)、账户转账。悲观锁在锁定期间提供最强的隔离性。
    • 数据竞争激烈,重试代价高: 如果业务逻辑复杂,回滚和重试的成本(如涉及多个系统调用、长事务)远高于加锁的成本。
    • 短事务且冲突高发的小范围数据操作。
  2. 乐观锁适用场景:

    • 读多写少: 这是最典型的场景。例如:
      • 电商商品库存扣减: 成千上万人浏览商品(读),只有少量人真正下单(写)。使用 version 字段在扣减库存时检查是否被修改,避免超卖。冲突少时性能极佳。
      • 论坛帖子、新闻内容更新: 大量用户阅读,只有作者或管理员偶尔编辑。使用乐观锁避免编辑冲突。
      • 用户信息更新(非核心字段): 如头像、昵称、个人简介更新,冲突概率低。
    • 冲突概率较低: 即使有写操作,但多个事务同时修改同一行数据的可能性不大。
    • 系统响应时间和吞吐量要求高: 需要最大化读并发能力。
    • 需要避免死锁: 系统设计复杂,难以完全规避死锁风险。
    • 分布式系统环境: 在分布式场景下实现悲观锁(如分布式锁)通常代价更高,乐观锁(基于版本号)更容易实现且性能更好。
    • 业务允许重试: 更新失败后,重新加载最新数据并再次尝试提交的操作在业务逻辑上是可接受的。例如,购物车结算时库存不足提示用户重试。

总结与选择建议:

  • 选择悲观锁: 当你非常确定数据会发生激烈竞争(写冲突频繁),或者业务逻辑无法容忍更新失败重试(要求一次性成功),或者重试的代价非常高时。
  • 选择乐观锁:读操作远多于写操作,或者写冲突发生的概率较低,或者系统对高并发读性能要求极高,或者你希望避免死锁风险,并且业务逻辑能够处理更新失败并重试时。

在实际应用中,乐观锁由于其在高并发读场景下的巨大优势,在互联网应用中更为常见,尤其是在数据库操作层面。悲观锁则更多用于对数据强一致性要求极高且冲突确实频繁的核心业务场景,或者在应用层代码中保护临界区资源。理解它们的原理和适用场景,有助于在设计和开发时做出更合适的选择,平衡性能、并发性和数据一致性。

Released under the MIT License.