又是一个BUG--记一个不知道咋起名的BUG

❓背景

起因是上一篇又是一个BUG--记一个用户被短信轰炸了一天的BUG 提到的线上事故,处理完后,我大概的查了下,所有涉及到人员报名 的地方都进行了重复报名校验,按理说不会出现同一用户多 次报名同一个项目的情况,上一篇最后也提了,我后来指定被打脸了

也是后来线上反馈又出现了同一个人多次报名的情况,看完线上数据,确认不是之前的脏数据,enmmm咱这次直接看代码吧,先大概喽一眼

java 复制代码
@Transactional(rollbackFor = Exception.class)
@Override
public void add(SignParam signParam) {


  Project project = projectService.getByNo(signParam.getProjectNo());
  /**
  * 中间省略一系列其他处理
  */
  Long studentId = LoginContextHolder.me().getSysLoginUserId();

  // 检测是不是已经报名、或报名冲突
  this.checkConflict(project, studentId);
  boolean b = addPeopleNum(project);
  if (!b) {
      throw new GpowerException("报名失败,项目报名人数达到上线!");
  }

  // 1.添加子项目报名记录
  List<ProjectDetail> projectDetailList = projectService.getProjectDetailList(signParam.getProjectNo());
  if (ObjectUtil.isNotEmpty(projectDetailList)) {
      List<SignDetail> list = new ArrayList<>(projectDetailList.size());
      for (ProjectDetail projectDetail : projectDetailList) {
          SignDetail signDetail = new SignDetail();
          signDetail.setSignType(SignTypeEnum.NORMAL.getCode());
          signDetail.setProjectDetailId(projectDetail.getId());
          signDetail.setProjectNo(signParam.getProjectNo());
          signDetail.setStudentId(studentId);
          signDetail.setDeptId(dept.getId());
          signDetail.setDeptName(dept.getAllName());
          list.add(signDetail);
      }
      signDetailService.saveBatch(list);
  }

  // 2.同步项目报名
  Sign sign = new Sign();
  BeanUtil.copyProperties(signParam, sign);
  sign.setSignType(SignTypeEnum.NORMAL.getCode());
  sign.setStudentId(studentId);
  // 报名后发公告
  CompletableFuture.runAsync(() -> {
      noticeService.addReceiveNoticeUser(project, 4, Arrays.asList(studentId));
  });
  this.save(sign);

  checkUserPhone(sign.getStudentId(), sign.getPhone());
}

浅看一下上边的代码后我们来提取下他的报名处理过程

1.this.checkConflict(project, studentId);进行了报名冲突,或者重复报名的验证(你看吧,我就说人做了验证了)

2.signDetailService.saveBatch(list);添加子项目报名记录

3.this.save(sign);进行项目的报名

4.checkUserPhone(sign.getStudentId(), sign.getPhone());不知道他们为啥把这个放最后了,搞不懂,搞不懂

在已知我被打脸的情况下,也就是用户还是重复报名了,那高低得去checkConflict()方法里喽一眼

java 复制代码
public void checkConflict(Project project, Long studentId) {
    List<Sign> signList = this.list(new QueryWrapper<Sign>().lambda()
            .eq(Sign::getProjectNo, project.getProjectNo())
            .eq(Sign::getStudentId, studentId)
            .ne(Sign::getSignType, SignTypeEnum.USER_DELETE.getCode()));
    if (CollectionUtil.isNotEmpty(signList)) {
        throw new ServiceException(ProjectExceptionEnum.DOUBLE_BAOMING);
    }
}

根据项目编号和用户id去查询报名记录,如果当记录不为空的时候抛出一个异常,提示请勿重复报名 所以要是按常理说这应该能起到作用的吧,毕竟开始就做判断了,然后我就去本地启动了下他的小程序端然后发现了个很哇塞的事,界面我就不放截图了,描述下问题,他那个报名的按钮吧,它能连点连点啊,哐哐哐一卡车请求都干后端去了

当时我觉得肯定就是它的问题了,因为后端立刻就提示了请勿重复报名,而且当我再看代码的时候发现了一个被我之前忽略的方法 boolean b = addPeopleNum(project);咱来瞅瞅这个

java 复制代码
private boolean  addPeopleNum(Project project) {
    String currentLock = PROJECT_LOCK_COMMENT_NAME + project.getProjectNo();
    boolean lock = false;
    long startTime = System.currentTimeMillis();
    long endTime;
    try {
        while (true) {
            // 循环获取锁,直至获取为止
            lock = RedisLockUtils.lock(currentLock, currentLock);
            endTime = System.currentTimeMillis() - startTime;
            if (lock) {
                break;
            }
            if (endTime > 1000 * 60) {
                throw new GpowerException(StrUtil.format("长时间未获取到project的锁【{}】,请稍后重试!", currentLock));
            }
        }
        Project project1 = projectService.getOne(Wrappers.lambdaQuery(Project.class).
                eq(Project::getProjectNo, project.getProjectNo()));
        Integer peopleNum = project1.getRegPeopleNum() + 1;
        if (peopleNum > project.getRegPeopleMax()) {
            return false;
        }
        project1.setRegPeopleNum(peopleNum);
        projectService.updateById(project1);
    } catch (Exception e) {
        log.error(e.toString());
    } finally {
        // 释放锁
        if (lock) {
            RedisLockUtils.releaseLock(currentLock);
        }
    }
    return true;
}

总结下这个方法做的事,大概就是加了锁 ,去处理项目剩余可报名的数量 ,相当于"减库存了",嘿嘿,就喜欢记得加锁的同志。 到目前为止我们知道,报名逻辑中重复报名的校验 有了,"减库存"的锁 也有了,哪怕在页面重复发送请求时也直接拦住了,那为啥上次被短信轰炸的用户有那么多条重复的?

不知道同志们有没得印象哈,前几篇帖子中有提到过,这个项目后端是两个节点,那当后端两个节点的时候,碰上前端批量请求会有什么效果呢?(图画的不咋好看,但大概是这个意思)

这样我们可以看到,当多节点碰上重复提交的时候,这个流程怎么走,当节点1节点2 同时接收到请求时,会前后脚的进行判断是否报名 的处理,这个时候两个节点应该都是未报名成功的状态,下边这还有个锁呀,它走的慢呀,两个请求在完全执行完之前,去判断重复报名,返回的都是未报名,来一批请求,只要跑第一的请求还没提交事务后边的请求就都能报名成功,嘿嘿,又有事务。 所以看到这,重复报名的事算破案了,两个点,

  1. 页面报名按钮连点,导致好多好多请求到了后端
  2. 后端的锁,只能保障"库存"的处理不出问题

然后就可以修BUG了,先提个禅道单子给前端,把按钮提交后给它置灰咯 ,再把后端给它加把大锁,把用户给锁住咯,严严实实的,这次指定能拦得住重复报名了,要是再出问题,再打脸,我就再瞅瞅代码,看看是不是还能整有个BUG让我写一篇出来,哈哈哈哈哈

所以一般在处理类似这种问题需要多考虑一下,毕竟可能写代码的时候是一个后端,生产上没准就是个就集群了哦,不能只是简单的判断重复报名吼,和锁的配合也得多关注下,一把锁,两把锁,大锁,小锁,金锁,小燕子,尔康...

(都看到这了,各位客官点个赞支持下呀)

相关推荐
葫芦和十三5 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp6 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑7 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯7 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan9 小时前
多Agent之间的区别
后端
青石路11 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充12 小时前
1.面向对象设计思想
后端
IT_陈寒12 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro12 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗13 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端