一、队伍已加入用户数量
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();
}