一、什么是乐观锁?
乐观锁是一种基于版本控制的并发控制机制。在乐观锁的思想中,认为数据访问冲突的概率很低,因此不加锁直接进行操作,但在更新数据时会进行版本比对,以确保数据的一致性。
乐观锁的原理主要基于版本号或时间戳来实现。在每次更新数据时,先获取当前数据的版本号或时间戳,然后在更新时比对版本号或时间戳是否一致,若一致则更新成功,否则表示数据已被其他线程修改,更新失败。
在Java中,常见的乐观锁实现是使用Atomic类,例如AtomicInteger、AtomicLong等。这些类提供了原子操作,可以确保对共享资源的更新操作是原子性的,从而避免了锁的开销和线程等待,另外,CAS(Compare-And-Swap)是实现乐观锁的核心算法,它通过比较内存中的值是否和预期的值相等来判断是否存在冲突。如果存在,则返回失败;如果不存在,则执行更新操作。Java中提供了AtomicInteger、AtomicLong、AtomicReference等原子类来支持CAS操作。
二、乐观锁适用于什么场景
写操作较少:在这种场景下,多个事务或线程大部分时间都在读取数据,而写操作的频率相对较低。乐观锁能够减少锁的持有时间,允许多个事务或线程同时读取数据,而不会相互阻塞。
数据冲突较少:如果数据更新操作之间的冲突较少,即多个事务或线程同时更新同一份数据的概率较低,那么乐观锁能够发挥很好的性能。因为即使偶尔出现冲突,也只是在更新数据时才会被检测到,而不需要在整个数据处理过程中都锁定资源。
重试成本较低:乐观锁在检测到冲突时会回滚事务或提示冲突,需要客户端重新尝试更新操作。因此,如果重试的成本较低(例如,重试不会导致大量计算或I/O操作),那么使用乐观锁是合适的。
系统能够容忍一定程度的失败:由于乐观锁在更新数据时可能会因为版本冲突而失败,因此系统需要能够处理这种失败情况。如果系统能够容忍一定程度的失败(例如,通过重试或其他补偿机制来恢复),那么使用乐观锁是可行的。
三、乐观锁优缺点
优点:
高并发高吞吐:乐观锁不会阻塞其他事务的读取操作,只在提交时检查数据是否被修改,因此可以提供更好的并发性能。
无锁操作:乐观锁不需要显式地获取和释放锁,减少了锁竞争和上下文切换的开销。
无死锁风险:由于乐观锁不会阻塞其他事务的访问,因此不会出现死锁的情况。
缺点:
冲突处理复杂:由于乐观锁不会阻塞其他事务,因此在提交时需要检查数据是否被其他事务修改,如果发现冲突,需要回滚事务或重新尝试操作,这增加了冲突处理的复杂性。
数据一致性风险:乐观锁假设并发冲突较少,因此可能存在数据一致性的风险。如果多个事务同时对同一数据进行修改,可能会导致数据不一致的情况。
需要额外字段:为了实现乐观锁,通常需要在数据表中添加额外的版本号或时间戳字段,这增加了存储空间的需求。
处理不当造成死循环风险:在大多数业务中乐观锁更新失败都会进行自旋,如果没有控制好自旋退出逻辑可能会造成递归死循环问题。
四、使用示例
简单业务代码:
@Override
public String test1() {
SumDO sumDO = sumMapper.selectById(1);
if (sumDO.getSum() <= 0) {
log.info(sumDO.getSum() + ":库存不足");
return "库存不足";
}
Boolean update = sumMapper.updateByVersion(sumDO.getId(), sumDO.getVersion(), sumDO.getVersion() + 1);
if (update) {
System.out.println(update + ":购买成功,剩余数量 = " + (sumDO.getSum() - 1));
} else {
System.out.println(update + ":当前人数过多,修改失败 = " + (sumDO.getSum()));
}
return update.toString();
}
数据库修改语句
@Update("UPDATE `sum` SET `sum` = sum - 1 , version = #{newVersion} WHERE `id` = #{id} and version = #{version}")
Boolean updateByVersion(int id, int version,int newVersion);
五、验证结果
初始数据库数据 5个库存,版本号为1
通过20个线程并发测试
结果五个全部卖出
数据库库存为0,未产生超卖的情况