一、队伍已加入用户数量
1. 封装的响应对象 UserTeamVO 新增字段 hasJoinNum
2. 查询队伍 id 列表
3. 分组过滤,将 team_id 相同的 userTeam 分到同一组
4. 获取每一组的 userTeam 数量,即一个 team_id 对应几个userTeam(用户数量)
5. 设置加入的队员数量 hasJoinNum 返回给前端
// 查询加入队伍的用户数
QueryWrapper<UserTeam> userTeamJoinNumQW = new QueryWrapper<>();
userTeamJoinNumQW.in("team_id", teamIdList);
List<UserTeam> userTeamList = userTeamService.list(userTeamJoinNumQW);
// 队伍 id => 加入该队伍的用户列表
Map<Long, List<UserTeam>> teamIdUserTeamList = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
teamList.forEach(team -> {
team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(),new ArrayList<>()).size());
});
二、重复加入队伍的问题
1. 问题:高并发场景下,用户疯狂点击加入队伍,可能会重复加入同一个队伍
- 一个请求开启一个线程,多次点击加入队伍,多个线程进入,判断用户是否已加入该队伍时都是未加入,都去执行加入队伍的业务
- 出现同一个用户重复加入同一个队伍的情况,用户 - 队伍关系表中添加了多条记录,且已加入队伍的人数异常增加
2. 解决:使用 synchronized 关键字给判断队伍和加入队伍这段逻辑加锁
3. 优化:调整锁的粒度,分析锁的范围
- 不同用户可以加入不同队伍,如果给整段都加上锁,不同用户加入时可能会阻塞,降低性能
- 锁用户:同一个用户不能重复加入同一个队伍
- 锁队伍:同一个用户不能同时加入多个队伍(否则可能突破“每个用户最多创建和加入 5 个队伍的限制”)
注意:数据库插入数据之前,判断的都是用户未加入该队伍 / 用户创建和加入的队伍不满 5 个,如果把锁用户和锁队伍分开,一个线程拿到锁之后判断用户,结束判断去获取队伍的锁,线程 2 就可以拿到用户锁了,但是线程 1 还没有结束插入数据的业务(队伍锁),线程 2 也可以执行,不过是多等了一会,所以锁的范围要到将数据插入数据库完成才能释放锁,之后其他线程获取到锁再去判断数据库,此时数据库已经更改,判断才是有效的
4. 仍存在问题
- synchronized 同步锁是 JVM 提供的,保存在 JVM(常量池)中,集群模式下,每台 JVM 不共享锁数据,每台 JVM 都可以有一个线程获取到同步锁,锁失效
- 解决方法:使用 Redisson 提供的分布式锁,或 Redis 自主实现分布式锁(存在问题,但大多数场景可用)
5. 使用分布式锁解决重复加入队伍的问题
- 创建 RedissonClient 客户端实例
- 获取锁对象
- 尝试获取锁:获取成功执行业务,失败等待重试或直接返回
- 释放锁
// 1. 用户最多创建和加入 5 个队伍
RLock lock = redissonClient.getLock(JOIN_TEAM_USER_LOCK);
try {
while (true) {
// 获取到锁执行业务
if (lock.tryLock(0,-1, TimeUnit.MILLISECONDS)) {
log.info("get redisson lock" + Thread.currentThread().getId());
Long userId = loginUser.getId();
QueryWrapper<UserTeam> userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("user_id", userId);
long hasJoinNum = userTeamService.count(userTeamQueryWrapper);
if (hasJoinNum >= 5) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户最多创建和加入 5 个队伍");
}
// 2. 队伍必须存在,只能加入未满、未过期的队伍
Long teamId = teamJoinRequest.getTeamId();
Team team = this.getTeamById(teamId);
userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("team_id", teamId);
long teamHasJoinNum = userTeamService.count(userTeamQueryWrapper);
if (teamHasJoinNum >= team.getMaxNum()) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍人数已满");
}
Date expireTime = team.getExpireTime();
if (expireTime != null && expireTime.before(new Date())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "队伍已过期");
}
// 3. 不能加入自己的队伍,不能重复加入已加入的队伍(幂等性)
// if (team.getUserId() == userId) {
// throw new BusinessException(ErrorCode.PARAMS_ERROR,"不能加入自己创建的队伍");
// }
userTeamQueryWrapper = new QueryWrapper<>();
userTeamQueryWrapper.eq("team_id", teamId);
userTeamQueryWrapper.eq("user_id", userId);
long alreadyJoinNum = userTeamService.count(userTeamQueryWrapper);
if (alreadyJoinNum > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户已加入该队伍");
}
// 4. 禁止加入私有的队伍
Integer status = team.getStatus();
TeamStatusEnum teamStatusEnum = TeamStatusEnum.getTeamEnumByValue(status);
if (teamStatusEnum.equals(TeamStatusEnum.PRIVATE)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "禁止加入私有的队伍");
}
// 5. 如果加入的队伍是加密的,必须密码匹配才可以
String password = teamJoinRequest.getPassword();
if (teamStatusEnum.equals(TeamStatusEnum.SECRET)) {
if (StringUtils.isBlank(password) || !password.equals(team.getPassword())) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
}
}
// 6. 新增队伍 - 用户关联信息
UserTeam userTeam = new UserTeam();
userTeam.setUserId(userId);
userTeam.setTeamId(teamId);
userTeam.setJoinTime(new Date());
return userTeamService.save(userTeam);
}
}
} catch (InterruptedException e) {
log.error("redisson join team error", e);
return false;
} finally {
log.info("redisson unlock" + Thread.currentThread().getId());
lock.unlock();
}