乐观锁是什么,给出实现思路

乐观锁的核心思想是:它假设并发冲突是小概率事件。因此,在数据处理过程中,它不会对数据进行加锁,允许多个事务或线程同时读取和修改数据。只有在最终提交更新的时候,才会去检查在这期间是否有其他事务或线程修改了这份数据。如果数据没有被修改,则提交成功;如果数据已经被修改,则认为发生了冲突,当前操作失败,然后应用程序通常会根据业务需求选择重试、报错或让用户决定如何处理。 与悲观锁("先获取锁,再访问数据")不同,乐观锁是"先访问数据,提交时再检查冲突"。

实现乐观锁的常见思路主要有两种:

  1. 版本号机制 (Versioning)

    • 思路:
      1. 在数据表中增加一个整型或时间戳类型的"版本"字段(例如 versionupdate_time)。
      2. 当读取数据时,将这个版本号一同读出。
      3. 当进行更新操作时,将之前读出的版本号作为条件加入到更新语句的 WHERE 子句中。同时,在 SET子句中将版本号加1(或者更新为当前时间戳)。
      4. 执行更新。如果 WHERE 子句中指定的版本号与数据库中当前的版本号一致,说明在读取数据到准备更新的这段时间内,数据没有被其他线程修改过,更新成功,版本号也随之更新。
      5. 如果 WHERE 子句中指定的版本号与数据库中当前的版本号不一致,说明数据已经被其他线程修改并更新了版本号,此时更新操作会因为条件不满足而不会执行任何行的更新(即rows_affected为0)。
    • 冲突处理:如果更新操作返回的影响行数为0,则表示发生了冲突。应用程序需要捕获这种情况,并决定下一步操作,例如:
      • 重新读取最新数据,重新尝试业务逻辑和更新(重试)。
      • 提示用户数据已更新,请刷新后重试。
      • 放弃本次操作。
  2. CAS (Compare-And-Swap) 机制

    • 思路:CAS 是一种原子操作,它包含三个操作数:内存位置V、预期原值A和新值B。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。
    • 在应用层面,尤其是不直接操作硬件原子指令的场景(如数据库),CAS的思想可以这样体现:
      1. 读取数据时,记录下关键字段的当前值(例如,不仅仅是版本号,也可以是某些核心业务字段的旧值)。
      2. 当进行更新操作时,在 WHERE 子句中检查这些关键字段的值是否仍然是之前读取到的旧值。
    • 这种方式其实是版本号机制的一种特例,或者说当没有显式版本号字段时的一种替代方案。但它要求 WHERE 子句中的所有比较字段都没有在读取后、更新前被修改。如果字段较多,SQL会变得复杂,且效率可能不如单一版本号字段。
    • 许多CPU指令集直接支持CAS原子操作,这在无锁数据结构和并发编程中非常重要,例如Java的java.util.concurrent.atomic包下的类就大量使用了CAS。

乐观锁的实现关键点: * 选择一个或多个能代表数据状态的标记(版本号、时间戳、具体业务字段值)。 * 在读取时获取这些标记。 * 在更新时,验证这些标记没有发生变化,并将更新标记和更新业务数据作为一个原子操作提交。 * 应用程序必须能够处理更新失败(即冲突发生)的情况。

乐观锁的优点: * 在冲突较少的情况下,它避免了悲观锁的加锁开销(如线程阻塞、上下文切换),可以提高系统的吞吐量。 * 由于大部分时间不加锁,死锁的概率大大降低。

乐观锁的缺点: * 如果冲突频繁发生,会导致大量的重试,反而降低性能,因为操作失败后需要回滚或重做,这部分是额外开销。 * ABA问题:如果一个值从A变为B,然后又变回A,使用CAS检查时会认为数据没有改变,但实际上它已经被修改过了。版本号机制通常能避免ABA问题,因为版本号是单向递增的。对于CAS,可以通过引入更复杂的版本号(如带有时间戳或序列号的标记)来缓解。

适用场景: 乐观锁更适用于读多写少的场景,即数据竞争不激烈的情况。例如,很多内容管理系统、商品库存的更新(在用户下单扣减库存的瞬间)等。

拓展延申: 在分布式系统中,乐观锁的思想也非常重要。例如,在使用如Etcd或ZooKeeper进行分布式协调时,它们通常也提供了基于版本号的CAS操作(如Etcd的mod_revision)。当多个服务实例尝试更新同一个配置项时,可以通过检查mod_revision来确保更新是基于最新的认知,避免覆盖掉其他实例的合法修改。

另外,在一些NoSQL数据库如DynamoDB中,也内置了条件写入(Conditional Writes)的功能,这本质上也是乐观锁的一种体现,允许你在写入数据时指定某些属性必须满足特定条件(如版本号匹配或属性值等于某个预期值),否则写入失败。这种机制对于维护数据一致性非常关键。