企業應用中,涉及到修改狀態的場景太多了。比如,企業入網後,要審核資質。個人領取任務後,企業管理員要審核領取人。 應用管理系統中,通常是下圖這樣,在列表後有操作按鈕來修改數據記錄的狀態。 點擊“通過”/“拒絕”操作,要修改數據記錄的status欄位。服務端程式邏輯怎麼實現呢? 先定義服務端api介面: ...
企業應用中,涉及到修改狀態的場景太多了。比如,企業入網後,要審核資質。個人領取任務後,企業管理員要審核領取人。
應用管理系統中,通常是下圖這樣,在列表後有操作按鈕來修改數據記錄的狀態。
點擊“通過”/“拒絕”操作,要修改數據記錄的status欄位。服務端程式邏輯怎麼實現呢?
先定義服務端api介面:
系統採用前後端分離,系統架構是分散式的。SpringMVC的RestController通過遠程調用Dubbo介面,來實現數據的CRUD。
項目使用jeecg-boot逆向工程生成代碼,然後在此基礎上進行調整。
程式邏輯v1
@RequestMapping(value = "/enterprise/taskApply/audit",method = RequestMethod.POST) public Result audit(@RequestBody TaskApplyVO taskApplyVO, HttpServletRequest req){ //當前登錄企業 EnterpriseVO enterpriseVo = EnterpriseContext.getEnterpriseVo(); if(enterpriseVo == null){ return Result.error("未登錄"); } if(StringUtils.isEmpty(taskApplyVO.getApplyIds())){ return Result.error("未選擇"); } String[] applyIds = taskApplyVO.getApplyIds().split(","); for (String applyId : applyIds) { TaskApplyVO taskApply = new TaskApplyVO(); taskApply.setApplyId(Long.parseLong(applyId)); taskApply.setStatus(taskApplyVO.getStatus()); taskApply.setAuditTime(DateUtils.getDate()); taskApplyService.updateById(taskApply); } return Result.ok("成功"); }
其中,taskApplyService是遠程RPC介面實例。
系統使用了一段時間後,bug出現了————已經審核完了的記錄還能再審核。什麼情況下會出現這種情況呢?我們且不說。不過看代碼邏輯,我們可以發現,在修改一條記錄的狀態欄位之前並沒有判斷狀態欄位的初始值(在 待審核 狀態下 才能修改為 審核通過or審核拒絕),所以會出現這種情況。
程式邏輯v2
修複上面的bug。加上前置狀態判斷。
幸好是這種常見的業務處理,fix掉即可。如果是在支付系統里,付款成功的交易還能被改成付款失敗,那可就出現資金損失了,哭都沒地兒哭去。
@RequestMapping(value = "/enterprise/taskApply/audit",method = RequestMethod.POST) public Result audit(@RequestBody TaskApplyVO taskApplyVO, HttpServletRequest req){ //當前登錄企業 EnterpriseVO enterpriseVo = EnterpriseContext.getEnterpriseVo(); if(enterpriseVo == null){ return Result.error("未登錄"); } if(StringUtils.isEmpty(taskApplyVO.getApplyIds())){ return Result.error("未選擇"); } String[] applyIds = taskApplyVO.getApplyIds().split(","); for (String applyId : applyIds) { TaskApplyVO taskApply = taskApplyService.getById(applyId); if(!TaskApplyStatusEnum.TO_AUDIT.name().equals(taskApply.getStatus())){ continue; } taskApply.setStatus(taskApplyVO.getStatus()); taskApply.setAuditTime(DateUtils.getDate()); taskApplyService.updateById(taskApply); } return Result.ok("成功"); }
系統使用了一段時間,bug出現了————用戶對數據記錄的修改莫名其妙的丟失了。哈哈,看上面的代碼,併發較多下,對這張數據表的同一條記錄的多個不同操作請求都出現時,比如這裡是審核,同時還有信息修改,是不是會出現覆蓋的情況?是的,因為在rpc調用getById與rpc調用updateById之間是有時間間隔的。兩個線程都通過getById取到了數據記錄,然後都修改了不同的欄位後執行updateById,資料庫操作本身是有先後的,所以,可能就會出現其中一次update覆蓋另一次update。那怎麼改呢?用分散式事務來控制?那可就比較費事了。為了減少這種衝突發生的可能性,還是先重構一下我們這個方法的邏輯吧。
程式邏輯v3
修改上面邏輯,new一個實體對象,只update所需欄位。
@RequestMapping(value = "/enterprise/taskApply/audit",method = RequestMethod.POST) public Result audit(@RequestBody TaskApplyVO taskApplyVO, HttpServletRequest req){ //當前登錄企業 EnterpriseVO enterpriseVo = EnterpriseContext.getEnterpriseVo(); if(enterpriseVo == null){ return Result.error("未登錄"); } if(StringUtils.isEmpty(taskApplyVO.getApplyIds())){ return Result.error("未選擇"); } String[] applyIds = taskApplyVO.getApplyIds().split(","); for (String applyId : applyIds) { TaskApplyVO byId = taskApplyService.getById(applyId); if(!TaskApplyStatusEnum.TO_AUDIT.name().equals(byId.getStatus())){ continue; } TaskApplyVO taskApply = new TaskApplyVO(); taskApply.setApplyId(Long.parseLong(applyId)); taskApply.setStatus(taskApplyVO.getStatus()); taskApply.setAuditTime(DateUtils.getDate()); taskApplyService.updateById(taskApply); } return Result.ok("保存成功"); }
系統使用了一段時間,bug出現了。什麼bug?還是最開始的bug————已經審核完了的記錄還能再審核。 !!!奔潰了~~
程式邏輯v4
使用狀態鎖。
同時,變更程式實現。將審核的邏輯後置封裝到RPC服務里。好的程式設計往往是把邏輯封裝起來,利於維護,也利於復用。
@RequestMapping(value = "/enterprise/taskApply/audit",method = RequestMethod.POST) public Result audit(@RequestBody TaskApplyVO taskApplyVO, HttpServletRequest req){ //當前登錄企業 EnterpriseVO enterpriseVo = EnterpriseContext.getEnterpriseVo(); if(enterpriseVo == null){ return Result.error("未登錄"); } if(StringUtils.isEmpty(taskApplyVO.getApplyIds())){ return Result.error("未選擇"); } String[] applyIds = taskApplyVO.getApplyIds().split(","); return taskApplyService.audit(applyIds, taskApplyVO.getStatus()); }
下麵是TaskApplyService實現類中的audit方法。可以看出來,每次迴圈還少了一次讀庫查詢。
@Override public Result audit(String[] taskIdArr, String auditStatus) { for (String id : taskIdArr) { TaskApply taskApply = new TaskApply(); taskApply.setStatus(auditStatus); taskApply.setAuditTime(DateUtils.getDate()); taskApplyManager.update(taskApply, new LambdaQueryWrapper<TaskApply>() .eq(TaskApply::getApplyId, Long.parseLong(id)) .eq(TaskApply::getStatus, TaskApplyStatusEnum.TO_AUDIT.name())); } return Result.ok(); }
我們修改mybatis-plus配置,把程式執行的sql列印出來
mybatis-plus:
configuration:
# 這個配置會將執行的sql列印出來,在開發或測試的時候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
如下是程式執行的sql。where條件里有status欄位。update執行成功返回1,否則返回0。
==> Preparing: UPDATE emax_task_apply SET apply_status=?, audit_time=? WHERE apply_id = ? AND status = ? ==> Parameters: TASKAPPLY_PASS(String), 2020-03-23 16:52:44.186(Timestamp), 1(Long), TO_AUDIT(String) <== Updates: 1
結束
感謝閱讀,本文整理用時2:51:00(18:00~20:51)。不足之處,歡迎交流!