乐观锁及mybatis-plus实现
Posted 乐观男孩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了乐观锁及mybatis-plus实现相关的知识,希望对你有一定的参考价值。
乐观锁及mybatis-plus实现
乐观锁与悲观锁
乐观锁:在修改数据时,总是持乐观态度,认为数据不会被其他人修改,只在真正进行数据更新前进行数据冲突的检测。如果发生冲突,则将异常结果向上层反馈(比如数据库更新返回0代表无数据更新),由上层逻辑进行处理。乐观锁适合读多写少的场景,可以提高程序的吞吐量。
悲观锁:在修改数据时,总是认为数据很可能会被其他人修改,所以在进行逻辑处理前,就对后续要更新的数据进行加锁,以防其他人对数据进行修改。实际上是使数据的变更操作转变成串行化的方式进行。悲观锁,具有强烈的独占和排他特性。
乐观锁的实现方式
版本号:为每条数据增加一个字段,记录该条数据的当前版本。当对数据进行更新前,先获取数据的版本号,更新时,检测数据库的版本号是否与先前获取的版本号一致,如果一致,则更新成功,否则更新失败。版本号一般使用自增序列或时间戳。但是使用时间戳时需要注意,当在极短的时间内有可能多条线程会同时更新成功,造成数据不一致。如时间戳精确到毫秒,在同一毫秒内多条线程进行数据更新,此时会都更新成功。使用版本号的方式适合读多写少的场景,写很少,数据冲突的概率就很低。
条件判断:在某些场景下,使用版本号的方式不一定适合。比如在电商应用扣减库存场景,由于在高并发下,同一时刻存在大量线程对库存进行扣减,此时很容易造成数据冲突(当前线程去更新数据时,有可能数据已经被其他线程修改,版本号发生了变更),导致只有一小部分的线程扣减库存成功,而大部分的线程无法扣减,下单失败,显然这是大家不想看到的。所以针对写多读少的场景,条件判断比较适合。条件判断,指在更新数据时,利用条件对数据进行检测,只有条件成立,才能更新成功,否则更新失败。比如扣库存场景,只要库存量充足,就应该扣减成功,所以条件就是库存量大于等于应扣数量,sql类似如下:update 库存表 set 库存量=库存量-扣减量 where id=商品id and 库存量≥扣减量。
乐观锁数据冲突处理办法
当数据库乐观锁检测到数据冲突时,一般反馈到应用的结果是更新数据为0条,应用可以根据是否为0进行处理,一般的处理方法有两种:
1、重试执行业务逻辑,重新提交事务;
2、抛出异常,事务回滚。
乐观锁的使用
分别说明基于版本号和基于条件的乐观锁使用。
项目环境:springboot+mysql+mybatis-plus+maven。
基于版本号的乐观锁使用
mybatis-plus插件提供了版本号方式的实现,只需要配置相应的拦截器和注解即可实现。通过拦截器对版本号进行更新。
一、项目准备
1、引入maven依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
2、增加拦截器
@Configuration
public class BeanInitConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
mybatis-plus-boot-starter依赖包在3.4.0以上,OptimisticLockerInterceptor已过期。所以在3.4.0及以下,应使用OptimisticLockerInterceptor:
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor(){
return new OptimisticLockerInterceptor();
}
3、实体增加version字段,并增加@Version注解
4、编写Controller、Service、Mapper层
Controller:
Service:
Mapper:
5、数据库表
二、测试
1、新增测试
(1)、浏览器访问新增接口
可以看到,返回值为1,代表数据库发生1条记录变更。
(2)、查看数据库记录
可以看到,数据库已经新增了一条记录,而且version字段已经正确被赋值(数据库设置version默认值为0)。
2、更新测试
(1)、浏览器访问更新接口
同样发生一条数据变更。
(2)、查看数据库记录
可以看到,version字段已经发生变更。
(3)、执行sql情况
可以看到,在更新数据前先查询版本号,更新时mybatis-plus的拦截器自动将version进行自增。
(4)、异常测试:将获取到的版本号改成小于当前数据库的版本号,再进行更新
再次访问更新接口,此时可以看到后台报了异常,并且数据更新数为0,因为当待更新数据版本号小于数据库的版本号时无法进行更新数据,这正是乐观锁的作用
3、并发测试
Controller层启动两个线程同时更改数据,统计更新成功的次数,然后对比数据库的版本增量是否与统计成功的次数一致。
(1)、Controller层启动两个线程,每个线程运行5秒
(2)、数据库初始状态
(3)、页面访问更新接口,等待运行结束
(4)、后台日志打印
更新成功共895次。
(5)、数据库最终状态
可见823+895=1718。
4、说明
(1)、插入的时候version字段可以为空,数据库设置默认值,更新的时候version字段不可为空,否则无法进行自增,乐观锁不会生效。
(2)、如果不是用mybatis提供的mapper进行操作,而是手写的sql进行数据的更新,需要自己在sql拼version的更新:
(3)、UserServiceImpl的updateUser方法,在该方法内调用另一个Service(或dao层)进行数据更新,当提交User的事务,如果发现数据冲突抛出异常时事务进行回滚,另一个Service的事务也会回滚,保证了事务的一致性。
条件判断方式的乐观锁使用
以扣库存场景进行演示。
1、Controller、Service、Mapper、Entity:
Controller:
Service:
Mapper:
Entity:
2、数据库初始化数据
3、浏览器访问扣减库存接口
4、后台日志
5、更新后数据库情况
可以看到,两个线程所有的扣减都成功,而且数据库数据也正确,这是因为库存量足够,每次扣减都可成功。
6、手动将数据库库存量调为500,再次调用扣减接口
当前库存量:
调用扣减接口,查看后台日志和数据库数据:
数据库情况
可以看到,最多只能有500次扣减成功,之后的扣减都已经失败。
源码分析
mybatis-plus能实现版本号的更新,主要通过配置的OptimisticLockerInnerInterceptor拦截器实现,该拦截器会在sql执行前被调用,所以实现版本号的更新逻辑就在该拦截器内。
计算新值:
设置新值:
以上是关于乐观锁及mybatis-plus实现的主要内容,如果未能解决你的问题,请参考以下文章