出现场景
并发问题:
期望值:0
可能结果值:0或500
为了解决这一问题,MyBatis-Plus中引入了乐观锁这一概念。
解决思路
乐观锁和悲观锁是在并发编程中用于处理并发访问和资源竞争的两种不同的锁机制!!
悲观锁(Pessimistic Lock)
悲观锁的基本思想是:在整个数据访问过程中,将共享资源锁定,以确保其他线程或进程不能同时访问和修改该资源。悲观锁的核心思想是"先保护,再修改"。在悲观锁的应用中,线程在访问共享资源之前会获取到锁,并在整个操作过程中保持锁的状态,阻塞其他线程的访问。只有当前线程完成操作后,才会释放锁,让其他线程继续操作资源。这种锁机制可以确保资源独占性和数据的一致性,但是在高并发环境下,悲观锁的效率相对较低。像 Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁 (OptimisticLock)
乐观锁的基本思想是:认为并发冲突的概率较低,因此不需要提前加锁,而是在数据更新阶段进行冲突检测和处理。乐观锁的核心思想是"先修改,后校验"。在乐观锁的应用中,线程在读取共享资源时不会加锁,而是记录特定的版本信息。当线程准备更新资源时,会先检查该资源的版本信息是否与之前读取的版本信息一致,如果一致则执行更新操作,否则说明有其他线程修改了该资源,需要进行相应的冲突处理。乐观锁通过避免加锁操作,提高了系统的并发性能和吞吐量,但是在并发冲突较为频繁的情况下,乐观锁会导致较多的冲突处理和重试操作。在 Java 中java.util.concurrent.atomic
包下面的原子变量类(比如AtomicInteger
、LongAdder
)就是使用了乐观锁的一种实现方式 CAS 实现的。
值得注意的是,悲观锁和乐观锁是两种解决并发数据问题的思路,不是具体技术!!!
具体技术和方案:
1. 乐观锁实现方案和技术
- 版本号/时间戳:为数据添加一个版本号或时间戳字段,每次更新数据时,比较当前版本号或时间戳与期望值是否一致,若一致则更新成功,否则表示数据已被修改,需要进行冲突处理。
- CAS(Compare-and-Swap):使用原子操作比较当前值与旧值是否一致,若一致则进行更新操作,否则重新尝试。
- 无锁数据结构:采用无锁数据结构,如无锁队列、无锁哈希表等,通过使用原子操作实现并发安全。
2. 悲观锁实现方案和技术
- 锁机制:使用传统的锁机制,如互斥锁(Mutex Lock)或读写锁(Read-Write Lock)来保证对共享资源的独占访问。
- 数据库锁:在数据库层面使用行级锁或表级锁来控制并发访问。
- 信号量(Semaphore):使用信号量来限制对资源的并发访问。
版本号乐观锁技术的实现流程
- 每条数据添加一个版本号字段version
- 取出记录时,获取当前 version
- 更新时,检查获取版本号是不是数据库当前最新版本号
- 如果是[证明没有人修改数据], 执行更新, set 数据更新 , version = version+ 1
- 如果 version 不对[证明有人已经修改了],我们现在的其他记录就是失效数据!就更新失败
使用MyBatis-Plus数据使用乐观锁
1.添加版本号更新插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
2.乐观锁字段添加@Version注解
注意: 数据库也需要添加version字段
ALTER TABLE USER ADD VERSION INT DEFAULT 1 ; # int 类型 乐观锁字段
支持的数据类型:
int | Integer | long | Long | Date | Timestamp | LocalDateTime |
支持的方法:
updateById(id) | update(entity, wrapper) |
pojo实体类中字段:
@Version
private Integer version;
3.使用
//乐观锁生效场景
@Test
public void testOptimisticLock(){
//步骤1: 先查询,在更新 获取version数据
//同时查询两条,但是version唯一,最后更新的失败
User user = userMapper.selectById(5);
User user1 = userMapper.selectById(5);
user.setAge(20);
user1.setAge(30);
userMapper.updateById(user);
//乐观锁生效,失败!
userMapper.updateById(user1);
}
总结
本文介绍了乐观锁和悲观锁的概念以及乐观锁常见实现方式:
悲观锁强调资源锁定,天然地对共享资源进行访问时,认为一定会发生冲突,因此在每次操作时都会加锁。这种锁机制阻塞其他线程地访问,只有当前线程完成操作后,才会释放锁。Java 中的 synchronized
和 ReentrantLock
是悲观锁的典型实现方式。虽然悲观锁能有效避免数据竞争,保证资源独占性和数据一致性,但在高并发场景下会导致线程阻塞、上下文切换频繁,效率相对较低,从而影响系统性能,并且还可能导致死锁产生。
乐观锁并不强调资源锁定,乐观地认为并发冲突的概率较低,共享资源在每次访问时不一定会发生冲突,因此不必提前加锁,只需在提交修改时数据更新阶段进行冲突检测和处理。Java 中的 AtomicInteger
和 LongAdder
等类通过 CAS(Compare-And-Swap)算法实现了乐观锁。乐观锁避免了加锁操作,从而解决了线程阻塞和死锁问题,提高了系统的并发性能和吞吐量,在读多写少的场景下性能十分出色。但在写操作频繁的情况下,乐观锁会导致较多的冲突处理和重试操作,可能出现大量重试和失败地场景,从而影响性能。
此外,乐观锁主要通过版本号/时间戳机制或 CAS 算法来实现,版本号机制通过设置一个版本号标志,在每次操作前先比较版本号来确保数据一致性,而另一种 CAS算法则是通过硬件指令实现原子操作,比较/交换变量值。
参考文章: