并发场景下会出现数据安全问题,我们经常通过加锁来保证数据的一致性,锁分为乐观锁和悲观锁
乐观锁
顾名思义,乐观锁比较乐观,假设冲突不会发生,但是在更新时会判断其他线程在这之前有没有对数据进行修改。实现方式有两种,分别是版本号法和CAS(Compare And Swap/Compare And Set)
版本号法
实现原理:
- 在数据库表中增加一个“version”字段,每次读取数据时都会获取当前版本号。
- 在更新数据时,将此版本号作为更新条件之一,检查当前版本号是否与读取时一致。
- 如果版本号一致,则执行更新操作,并将版本号加一;如果不一致,则说明在此期间有其他事务对数据进行了更新,当前更新操作应失败并可能需要重新读取数据尝试更新。
优点:
- 并发性能较高,不需要阻塞其他事务的执行。
- 实现简单,易于理解和维护。
缺点:
- 在高冲突率的场景下,可能需要频繁地回滚和重试,影响性能。
CAS
实现原理:
- CAS是一种无锁算法,通过比较并交换的方式实现对共享变量的原子更新。
- CAS操作包括三个操作数:被操作的内存值V、预期的值A和新值B。
- 当且仅当V的值等于A时,CAS才会将V的值更新为B,否则不进行任何操作。
优点:
- 无需加锁,避免了锁带来的开销和复杂性。
- 适用于高并发场景,能够提高系统性能。
缺点:
- 在高冲突率的场景下,可能会因为频繁的重试而导致性能下降。
- 需要硬件或特定编程语言的支持(如Java中的Atomic类)。
存在的问题及解决方案
-
循环时间太长和CPU开销:乐观锁通常通过自旋锁(spin lock)或CAS(Compare-And-Swap)操作来实现。在CAS操作中,如果数据在准备修改时被其他线程修改,则当前线程会重新尝试,这可能导致长时间的自旋等待。如果自旋时间过长且不成功,将大大增加CPU的开销。
- 解决方案:限制自旋次数,避免无限制的自旋等待,以减少CPU的浪费。
-
只能保证一个共享变量的原子操作:CAS操作通常只能保证对一个共享变量的原子性修改。如果需要对多个变量进行原子性操作,CAS则无法满足需求。
- 对于需要同时修改多个变量的场景,可以考虑使用其他同步机制,如悲观锁或更复杂的无锁算法。
-
ABA问题:一个共享变量在某个时间点的值是A,然后经过一系列操作后又变回了A,但在这个过程中,它的值可能已经被其他线程修改成了B(或其他值)。如果只检查变量的值是否相同,就会忽略掉中间的变化,从而导致数据不一致的问题。
- 解决方案:使用版本号或时间戳来记录变量的变化历史,从而避免ABA问题。在每次修改时,除了检查变量的值外,还需要检查版本号或时间戳是否一致。
悲观锁
悲观锁比较"悲观",假设冲突一定会发生,通过加(悲观)锁来确保某一时刻只有一个线程能读取并修改数据。常见的悲观锁有Java中的synchronized关键字,数据库中的表锁、行锁、页锁等等
乐观锁与悲观锁对比
乐观锁和悲观锁各有其适用场景和效率表现,不能一概而论地说乐观锁一定比悲观锁效率高。
乐观锁的效率
乐观锁通常适用于读多写少的场景,其核心思想是假设冲突不会频繁发生,只在数据更新时检查是否有冲突发生。如果没有冲突,则直接更新数据,避免了加锁的开销;如果有冲突,则可能需要重试更新操作。在冲突较少的情况下,乐观锁可以显著提高系统的吞吐量,因为它减少了锁的使用,降低了锁的竞争和等待时间。
悲观锁的效率
悲观锁则适用于写操作较多或冲突频繁的场景。它假设冲突总是会发生,因此在数据访问前就先加锁,以确保数据的一致性和完整性。悲观锁通过锁机制来避免冲突,但它也带来了额外的开销,包括锁的获取、释放和可能的锁等待时间。在高冲突的场景下,悲观锁可以确保数据的一致性,但可能会降低系统的吞吐量。
比较
- **在高冲突场景下,悲观锁可能更有效率。**因为它通过锁机制直接阻止了冲突的发生,避免了因冲突而导致的重试和回滚操作。
- **在低冲突场景下,乐观锁可能更有效率。**因为它避免了不必要的锁开销,减少了锁的获取、释放和等待时间。
结论
因此,在选择乐观锁还是悲观锁时,需要根据具体的应用场景、系统负载、事务大小、冲突频率以及数据库和存储引擎的支持情况等因素进行综合考虑。在某些情况下,乐观锁可能比悲观锁更有效率,但在其他情况下则可能相反。