最近項目有資金賬戶的相關需求,需要使用鎖做並發控制,借此機會整理下基於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);