• 改状态,你会改吗?你真的会改吗?


    企业应用中,涉及到修改数据记录的状态的场景太多了。比如,企业入网后,要审核资质。个人领取任务后,企业管理员要审核领取人。

    应用管理系统中,通常是下图这样,在查询列表后有操作按钮来修改数据记录的状态。

    点击“通过”/“拒绝”操作,要修改数据记录的status字段。服务端程序逻辑怎么实现呢?

    先定义服务端api接口:

    1. Request URL:
      http://***/api/enterprise/taskApply/audit
    2. Request Method:
      POST
    3. Content-Type:
      application/json;charset=UTF-8
    4. User-Agent:
      Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
    5. Request Payload:
      {"applyIds":"1,2,3","auditStatus":"PASS"}

    客户系统web采用前后端分离,后台应用系统架构是分布式的。SpringMVC的RestController通过远程调用Dubbo接口,来实现数据的CRUD。

    项目使用jeecg-boot逆向工程生成代码,然后在此基础上进行调整。

    程序逻辑v1

        @Reference
        private TaskApplyService taskApplyService;
    
        @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

    使用状态锁。

    这属于乐观锁的范畴,将原状态值放在update语句的where条件里,可以保证数据的强一致性。

    同时,变更程序实现。将审核的逻辑从MVC后置封装到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
        @Transactional
        public Result audit(String[] applyIdArr, String auditStatus) {
            for (String id : applyIdArr) {
                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)。不足之处,欢迎交流!

  • 相关阅读:
    Uva 10719 Quotient Polynomial
    UVa 11044 Searching for Nessy
    Uva 10790 How Many Points of Intersection?
    Uva 550 Multiplying by Rotation
    Uva 10916 Factstone Benchmark
    Uva 10177 (2/3/4)D Sqr/Rects/Cubes/Boxes?
    Uva 591 Box of Bricks
    Uva 621 Secret Research
    Uva 10499 The Land of Justice
    Uva 10014 Simple calculations
  • 原文地址:https://www.cnblogs.com/buguge/p/12554965.html
Copyright © 2020-2023  润新知