乐观锁及mybatis-plus实现

Posted 乐观男孩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了乐观锁及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实现的主要内容,如果未能解决你的问题,请参考以下文章

悲观锁及乐观锁

第四章Redis的事务锁及管理命令

利用乐观锁及redis解决电商秒杀高并发基本逻辑

AQS实现共享锁及CountDownLatch源码解析

Java并发编程常见锁及优化

数据库锁及实现