❓背景
起因是上一篇又是一个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 同时接收到请求时,会前后脚的进行判断是否报名 的处理,这个时候两个节点应该都是未报名成功的状态,下边这还有个锁呀,它走的慢呀,两个请求在完全执行完之前,去判断重复报名,返回的都是未报名,来一批请求,只要跑第一的请求还没提交事务 ,后边的请求就都能报名成功,嘿嘿,又有事务。 所以看到这,重复报名的事算破案了,两个点,
- 页面报名按钮连点,导致好多好多请求到了后端
- 后端的锁,只能保障"库存"的处理不出问题
然后就可以修BUG了,先提个禅道单子给前端,把按钮提交后给它置灰咯 ,再把后端给它加把大锁,把用户给锁住咯,严严实实的,这次指定能拦得住重复报名了,要是再出问题,再打脸,我就再瞅瞅代码,看看是不是还能整有个BUG让我写一篇出来,哈哈哈哈哈
所以一般在处理类似这种问题需要多考虑一下,毕竟可能写代码的时候是一个后端,生产上没准就是个就集群了哦,不能只是简单的判断重复报名吼,和锁的配合也得多关注下,一把锁,两把锁,大锁,小锁,金锁,小燕子,尔康...
(都看到这了,各位客官点个赞支持下呀)