【MybatisPlus】基于@Version注解的乐观锁实现


最近项目有资金账户的相关需求,需要使用锁做并发控制,借此机会整理下基于MybatisPlus @Version注解的乐观锁实现的方案,以及项目中遇到的坑

一.MybatisPlus 乐观锁的配置

  参考MybatisPlus(以下简称MP)官方文档,https://baomidou.com/pages/0d93c0/#optimisticlockerinnerinterceptor

 MP已经提供了乐观锁插件,使用起来很方便,只要两步即可完成配置

  MP乐观锁插件的配置

  1.配置乐观锁拦截器 OptimisticLockerInnerInterceptor,将拦截器Bean注入到到Spring容器中,MP官方提供了两种方案,一种是XML配置,另外一种是使用@Bean注解注入,这里使用第二种方式即@Bean方式注入

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    return interceptor;
}

  2.在实体类的字段上加上@Version注解

  例如:

@Version
private Integer version;

二.MybatisPlus 乐观锁的使用

  这里官方提供了两种方式

  1.使用updateById(entity)

  2.使用update(entity,wrapper)

  最终打印出来的SQL语句,除了原有的更新语句之后,还会根据@Version修饰的字段version自动加上update table set version=? where version=?

  例如

UPDATE account_sub_info SET version=?,balance=? WHERE (acc_sub_id = ? AND acc_id = ? AND version = ?) 

三. 踩坑指南(重点)

  在使用过程中遇到了几个点导致乐观锁更新失败

  1.在使用的updateById(entity)或update(entity,wrapper)这两个语句进行乐观锁控制时,参数entity的version属性必须为null则更新失败

    也就是说

    在新增时,我们需要为version字符赋默认值

    在更新时,我们可以在更新之前将数据先查一次,再使用上述方式根据查询结果的对象实体更新

  2.同一个方法中,对同一条数据执行一次查询,但是执行了两次更新,则第二次更新会因为乐观锁更新失败

  例如,

        LambdaQueryWrapper wrapper = Wrappers.lambdaQuery()
                .eq(AccountSubInfo::getAccSubId, 11094)
                .eq(AccountSubInfo::getAccId, 1248);
        AccountSubInfo accSubInfo = accountSubInfoService.getOne(wrapper,false);
        //第一次更新
        AccountSubInfo entity =new AccountSubInfo();
        entity.setAccSubId(accSubInfo.getAccSubId());
        entity.setAccId(accSubInfo.getAccId());
        entity.setDbId(accSubInfo.getDbId());
        entity.setBalance(accSubInfo.getBalance().add(new BigDecimal(10)));
        boolean result = accountSubInfoService.updateById(entity);
        //第二次更新
        entity.setBalance(accSubInfo.getBalance().add(new BigDecimal(10)));
        boolean result2 = accountSubInfoService.updateById(entity);

  原因是,在同一个方法中,第二次更新使用的实体对象AccountSubInfo的版本号仍然是更新前的版本号,所以导致更新失败

  解决方案:

        //第一次更新
        AccountSubInfo entity =new AccountSubInfo();
        entity.setAccSubId(accSubInfo.getAccSubId());
        entity.setAccId(accSubInfo.getAccId());
        entity.setDbId(accSubInfo.getDbId());
        entity.setBalance(accSubInfo.getBalance().add(new BigDecimal(10)));
        boolean result = accountSubInfoService.updateById(entity);
        //第二次更新
        entity.setVersion(entity.getVersion()+1); //特殊处理,修改版本号的预期值
        entity.setBalance(accSubInfo.getBalance().add(new BigDecimal(10)));
        boolean result2 = accountSubInfoService.updateById(entity);

  3.分库分表下的乐观锁实现

    sharding分库分表后,使用updateById(entity) 或 update(entity,wrapper) 都需要传入实体对象,

    这时会如果实体内包含了分片键,则会提示错误,不能更新分片键,如果没有包含分片键,则会走到所有分表的全路由,性能很低

    因此为了解决这个问题,这里对乐观锁机制进行手动处理,不使用上述两个updateById(entity) 或 update(entity,wrapper) 方法进行乐观锁控制

    参考:

        BigDecimal afterAmt = accSubInfo.getBalance().add(new BigDecimal(10));
        accSubInfo.setBalance(afterAmt);
        LambdaUpdateWrapper updateWrapper = Wrappers.lambdaUpdate()
                .eq(AccountSubInfo::getAccSubId, accSubInfo.getAccSubId()) //分片键字段
                .eq(AccountSubInfo::getAccId, accSubInfo.getAccId())
                .eq(AccountSubInfo::getVersion,accSubInfo.getVersion())    //版本号条件
                .set(AccountSubInfo::getVersion,accSubInfo.getVersion()+1) //设置版本号
                .set(AccountSubInfo::getBalance, afterAmt);
        boolean ret = accountSubInfoService.update(updateWrapper);