浪花 - 后端接口完善

一、队伍已加入用户数量

1. 封装的响应对象 UserTeamVO 新增字段 hasJoinNum

2. 查询队伍 id 列表

3. 分组过滤,将 team_id 相同的 userTeam 分到同一组

4. 获取每一组的 userTeam 数量,即一个 team_id 对应几个userTeam(用户数量)

5. 设置加入的队员数量 hasJoinNum 返回给前端

// 查询加入队伍的用户数
QueryWrapper userTeamJoinNumQW = new QueryWrapper<>();
userTeamJoinNumQW.in("team_id", teamIdList);
List userTeamList = userTeamService.list(userTeamJoinNumQW);
// 队伍 id => 加入该队伍的用户列表
Map> teamIdUserTeamList = userTeamList.stream().collect(Collectors.groupingBy(UserTeam::getTeamId));
teamList.forEach(team ->  {
    team.setHasJoinNum(teamIdUserTeamList.getOrDefault(team.getId(),new ArrayList<>()).size());
});

浪花 - 后端接口完善_第1张图片

二、重复加入队伍的问题

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 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();
}

你可能感兴趣的:(浪花,-,后端,windows,java,linux)