浪花 - 后端接口完善

一、队伍已加入用户数量

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/353310.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构与算法教程,数据结构C语言版教程!(第六部分、数据结构树,树存储结构详解)三

第六部分、数据结构树&#xff0c;树存储结构详解 数据结构的树存储结构&#xff0c;常用于存储逻辑关系为 "一对多" 的数据。 树存储结构中&#xff0c;最常用的还是二叉树&#xff0c;本章就二叉树的存储结构、二叉树的前序、中序、后序以及层次遍历、线索二叉树、…

APUE学习之管道(pipe)与命名管道(fifo)

目录 一、简介 二、管道&#xff08;Pipe&#xff09; 1、管道的基本概念 2、管道的局限性 3、管道的创建 4、管道的读写规则 5、实战演练 三、命名管道&#xff08;fifo&#xff09; 1、命名管道的基本概念 2、命名管道的创建 3、实战演练 4、运行结果 四、补充 …

flyway使用配置参数和注意事项介绍

文章目录 业务场景参数介绍initSqlsbaselineOnMigratebaselineVersiontargetvalidateOnMigrate SQL注意事项 业务场景 对于生产环境&#xff0c;随着项目版本迭代&#xff0c;数据库结构也会变动。如果一个项目在多个地方实施部署&#xff0c;且版本不一致&#xff0c;就需要一…

lqb日志08

一只小蒟蒻备考蓝桥杯的日志 文章目录 笔记坐标相遇判断工作调度问题&#xff08;抽象时间轴绘制&#xff09; 刷题心得小结 笔记 坐标相遇判断 我是小懒虫&#xff0c;碰了一下运气&#xff0c;开了个“恰当”的数&#xff08;7000&#xff09;如果&#xff0c;7000次还不能…

使用sdbg执行smali简单片段解混淆

https://github.com/CalebFenton/simplify/releases/download/v1.3.0/sdbg-0.1.0.jar "C:\Program Files\Java\jre-1.8\bin\java.exe" -jar sdbg-0.1.0.jar smali "Lu/ad;->c()V"其中smali为文件夹名称。 ###### Class p124u.C12414ad (u.ad) .class …

Modern C++ std::unique_ptr的实现原理

​ unique_ptr是一个非常简单的类,没有计数没有原子操作,非常类似纯指针。 它的类定义也非常简单: 它针对数组做了模板偏特化,因为它得支持数组操作比如Arr[i]。 unique_ptr的本质就是std::tuple, 里面第一项为指针指向管理对象,第二项为deleter:是一个函数指针或仿函数…

电脑屏幕色彩调整

显卡驱动 如果你的电脑是笔记本且没有独显直连&#xff0c;那你就不能在独显里面调屏幕色彩&#xff0c;就要去下载对应核显的驱动&#xff0c;然后去核显的驱动程序里面可以调节。比如&#xff1a;我的笔记本是华硕天选2&#xff0c;无独显直连&#xff0c;锐龙处理器&#x…

Nginx基础篇【一】

Nginx基础篇【一】 一、Nginx基础篇【一】1.1.背景介绍1.2.名词解释1.2.1. WEB服务器&#xff1a;1.2.2. HTTP:1.2.3. POP3/SMTP/IMAP&#xff1a;1.2.4. 反向代理1.2.5.常见服务器对比1.2.5.1.IIS1.2.5.2.Tomcat1.2.5.3.Apache1.2.5.4.Lighttpd1.2.5.5.其他的服务器 1.3.Nginx…

跟着cherno手搓游戏引擎【12】渲染context和首个三角形

渲染上下文&#xff1a; 目的&#xff1a;修改WindowsWindow的结构&#xff0c;把glad抽离出来 WindowsWindow.h:新建m_Context #pragma once #include "YOTO/Window.h" #include <YOTO/Renderer/GraphicsContext.h> #include<GLFW/glfw3.h> #include…

Ps:将文件载入堆栈

Ps菜单&#xff1a;文件/脚本/将文件载入堆栈 Scripts/Load Files into Stack 将文件载入堆栈 Load Files into Stack脚本命令可用于将两个及以上的文件载入到同一个 Photoshop 新文档中。 载入的每个文件都将成为独立的图层&#xff0c;并使用其原始文件名作为图层名。 Photos…

GraphicsMagick 的 OpenCL 开发记录(二十五)

文章目录 如何修复R6025 pure virtual function call问题 <2022-04-19 周二> 如何修复R6025 pure virtual function call问题 运气好&#xff0c;修复了这个问题。即&#xff0c;在ExitInstance()函数中调用一下MagickLib::DestroyMagick();即可。 过程中也经历了尝试…

CSS探索浏览器兼容性

学习如何探索浏览器的兼容性对于编写跨浏览器兼容的CSS代码非常重要。以下是一些学习CSS兼容性的方法&#xff1a; MDN文档&#xff1a;Mozilla开发者网络&#xff08;MDN&#xff09;提供了广泛而详细的CSS文档&#xff0c;其中包含有关CSS属性、选择器和功能的信息。在MDN上…

最新技术实战 | 无视杀软使用远控工具进行横向移动Tips

最新技术实战 | 无视杀软使用远控工具进行横向移动Tips。 杀软是什么意思&#xff1f;杀软是杀毒软件的简称&#xff0c;取的杀毒首字与软件首字组合而成&#xff0c;将杀毒软件简要的称之为杀软&#xff0c;所以&#xff0c;杀软的意思就是杀毒软件&#xff0c;专注于信息领域…

day34_js

今日内容 0 复习昨日 1 事件 1.1 事件介绍 1.2 事件绑定方式 1.3 不同事件的演示 2 DOM操作 2.1 概述 2.2 查找元素 2.3 元素内容的查找和设置 2.4 元素属性的查找和设置 2.5 元素CSS样式的查找和设置 2.6 创建元素 2.7 创建文本节点 2.8 追加元素 2.9 删除元素 3 案例练习 0 复…

基于springboot+vue的明星周边产品销售网站(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 研究背景…

如何快速在阿里云上更新幻兽帕鲁服务器?

如何快速在阿里云上更新幻兽帕鲁服务器&#xff1f;幻兽帕鲁更新之后&#xff0c;服务器需要同步更新才能继续游戏&#xff0c;大家可以按照文章操作完成服务升级。 1、如果大家是通过阿里云计算巢部署的&#xff0c;请参考&#xff1a;计算巢部署更新方式。 2、如果不是通过阿…

前端——HTML

目录 文章目录 前言 一.HTML的基本标签 二.HTML标签 1.块级标签 1.1块级标签特征 1.2标题标签 ​编辑 1.3 水平线标签 1.4 段落标签 1.5 无序列表标签 1.6 有序列表标签 1.7 表格标签 1.8层标签 1.9 表单 2. 行级标签 2.1行级标签特征 2.2图像标签 2.3 范围…

epoll示例

一、服务端 下面是一个使用epoll机制在Linux上编写的简单套接字程序示例&#xff1a; #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include &l…

一天吃透面试八股文

内容摘自我的学习网站&#xff1a;topjavaer.cn 分享50道Java并发高频面试题。 线程池 线程池&#xff1a;一个管理线程的池子。 为什么平时都是使用线程池创建线程&#xff0c;直接new一个线程不好吗&#xff1f; 嗯&#xff0c;手动创建线程有两个缺点 不受控风险频繁创…

滴水逆向三期笔记与作业——02C语言——10 Switch语句反汇编

滴水逆向三期笔记与作业——02C语言——10 Switch语句反汇编 一、Switch语句1、switch语句 是if语句的简写2、break加与不加有什么特点?default语句可以省略吗&#xff1f;3、游戏中的switch语句&#xff08;示例&#xff09;4、添加case后面的值&#xff0c;一个一个增加&…