最近项目有资金账户的相关需求,需要使用锁做并发控制,借此机会整理下基于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<AccountSubInfo> wrapper = Wrappers.<AccountSubInfo>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<AccountSubInfo> updateWrapper = Wrappers.<AccountSubInfo>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);