又是一个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让我写一篇出来,哈哈哈哈哈

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

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

相关推荐
monkey_meng1 分钟前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
草莓base13 分钟前
【手写一个spring】spring源码的简单实现--bean对象的创建
java·spring·rpc
Estar.Lee16 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
drebander38 分钟前
使用 Java Stream 优雅实现List 转化为Map<key,Map<key,value>>
java·python·list
乌啼霜满天24941 分钟前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
tangliang_cn1 小时前
java入门 自定义springboot starter
java·开发语言·spring boot
程序猿阿伟1 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
Grey_fantasy1 小时前
高级编程之结构化代码
java·spring boot·spring cloud
新知图书1 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
弗锐土豆1 小时前
工业生产安全-安全帽第二篇-用java语言看看opencv实现的目标检测使用过程
java·opencv·安全·检测·面部