基于SpringCloud Alibaba 分布式微服务选课系统的性能优化

1.引言

1.1编写目的

为了解决学生选课时系统崩溃 宕机的问题 特此编写本系统 实现选课系统的高可用与负载均衡

1.2背景

我们学校的选课系统人一多就会崩溃

分析原因还是因为一瞬间大量人涌入选课系统 系统扛不住高并发而宕机

本系统在此前期下 准备自己开发一款选课系统 使用sentinel作为服务熔断和降级框架 使用redisson作为分布式锁 使用Nginx+SpringCloud Gateway 做负载均衡``使用docker+k8s做持久化自动部署 保证几万学生在同一时间下 访问系统不会崩溃 宕机 使用JWT作为(Token)作为系统的身份校验 授权与认证 采用前后端分离的方式部署该系统,小组分工明确

1.3定义

(列出本文件中用到的专门术语的定义和外文首字母组词的原词组。)

前端

后端

部署

测试运维

1.4参考资料

Spring官方文档https://spring.io/

MyBatisPuls官方文档: https://baomidou.com/

Nacos官方文档https://nacos.io/zh-cn/

Sentinel官方文档https://sentinelguard.io/zh-cn/

SpringCloud中文文档: https://www.springcloud.cc/

Docker官方文档https://www.docker.com/

Vue官方文档https://cn.vuejs.org/

elementUI官方文档: https://element.eleme.cn/#/zh-CN

HuTools官方文档: https://hutool.cn/docs/#/

Kubernetes中文社区: http://docs.kubernetes.org.cn/

Cloud-Platform框架文档: https://gitee.com/geek_qi/cloud-platform/blob/master/dev-doc.md

2.项目概述

2.1 工作内容

完成选课的发布 学生登录系统 及学生选课 功能 学生可查看已选课程

扩展功能: 本校学生可登陆进本系统查看其课程表 校外考试 校内考试成绩 及绩点

2.2小组成员

软工1905 风 离: 项目后端 技术选型 文档编写 项目部署 运维 测试

技术水平: 掌握常用JAVA后端技术栈 能独立开发出分布式微服务系统 有过部署经验

软工1904 苏才华: 项目前端 思路提供者 UI设计师 前端大佬

技术水平:

软工1905 任威: 项目组织者 思路提供者 LOGO设计师

技术水平:

2.3 产品

基于SpringCloudalibaba分布式微服务 高并发选课系统

2.3.1 程序

(列出需移交给用户的程序的名称,所用的编程语言及存储程序的媒体形式,并通过引用有关文件,逐项说明其功能和能力。)

2.3.2 文件

(列出需移交给用户的每种文件的名称及内容要点。)

2.3.3 服务

(列出需向用户提供的各项服务,如培训安装、维护和运行支持等,应逐项规定开始日期、所提供支持的级别和服务的期限。)

2.3.4 非移交的产品

程序源码

2.4 验收标准

(对于上述这些应交出的产品和服务,逐项说明或引用资料说明验收标准。)


3. 实施计划

3.1工作任务的分解与人员分工

(对于项目开发中需完成的各项工作,从需求分析、设计、实现、测试直到维护,包括文件的编制、审批、打印、分发工作,用户培训工作,软件安装工作等,按层次进行分解,指明每项任务的负责人和参加人员。)

各项工作负责人参与人员
需求分析风离苏才华,任威
数据库设计风离苏才华,任威
后端接口实现风离
前端接口实现苏才华
前后端联调风离,苏才华
测试风离苏才华,任威
维护风离
文件的打印、分发工作
文件的编制、审批
用户培训工作
软件安装工作风离苏才华,任威

3.2 接口人员


接口类型人员职责
负责本项目同用户的接口
负责本项目同本单位各管理机构
负责本项目同各分合同负责单位的接口

3.3 进度

  • 需求分析与数据库设计 2.27
  • 项目前后端框架选取与搭建完毕 2.28
  • 解决前后端跨域联调 2.28
  • 解决Token传输问题 用户信息接口完成 3.01
  • 整合Redisson完成课程信息发布 已选课程信息接口完成 整合SpringCache完成缓存优化 3.02
  • 全部选课预热已完成(已测试) 3.04
  • 选课已解决(待高并发测试)3.06
  • 解决重复选课 删除缓存预热完成 发布选课班 展示待参与选课班级完成 3.07
  • 实现线上线下环境切换 整合MongoDB实现教务系统与本地选课系统的整合 3.09
  • 实现全云端配置文件 3.10
  • 创建高并发测试案例 并通过测试 3.11

3.4 预算

暂无

3.5关键问题

1: 中间件RabbitMQ宕机不可用问题

2:缓存数据库 Redis 持久化问题

3: 集群部署上线问题

4:高并发线下实测 待测

4.支持条件

4.1计算机系统支持

本地开发环境: Win10

远程部署环境: Linux - Centos 7

4.2需由用户承担的工作

使用选课系统

4.3需由外单位操供的条件

暂未

5.专题计划要点

待完成

我是分割线———————————

项目相关

项目截图及在线展示

img

image-20220311233621686

image-20220311233641926

img

img

img

img

image-20220310232635063

技术选型

项目前端

vue-element-admin

项目后端:

项目基础框架: SpringCloud SpringCloudAlibaba SpringBoot MybatisPlus

消息中间件: RabbitMQ

缓存数据库: Redis SpringCache Redisson

对象储存: SpringAlibaba OSS

安全框架:Spring Security

网关: Spring Cloud Gateway

远程调用RPC: OpenFeign

服务降级熔断: SpringCloudAliBaba Sentinel

服务注册中心与配置中心: SpringCloudAliBaba Nacos

链路追踪: spring-cloud-sleuth-zipkin

项目健康状态监控: Spring Boot actuator SpringBoot Admin

数据库及连接池:Mysql Druid

工具包: Hutools FastJson

日志:Log4j

项目脚手架:开源项目Cloud-Platform

定时任务框架:Quartz

项目协作

版本控制平台: Gitee

接口文档管理: ApiFox

远程办公: ToDesk

项目上线发布

部署环境:阿里云 Linux Centos (2核4G)* 2+ 阿里云 Linux Centos (1核2G) * 2+ 腾讯云 Linux Centos (4核16G)* 1

部署工具: Docker + K8s + Jenkins

测试工具:Jmeter

本地部署运行说明

前置条件: JDK1.8 Node.js 14.16.7 Maven 3.5.X Git

初始化Git仓库: >git init

前端拉取命令:

git clone -b vue_dev https://gitee.com/fl1906249647/course-selection-system.git
  • 下载依赖及运行(在下载目录下): npm install npm run dev

后端拉取命令:

git clone -b dev https://gitee.com/fl1906249647/course-selection-system.git
  • 修改配置文件

    yaml中所有的环境 Nacos RabbitMq Redis Mysql MongDB 账号密码端口为自己电脑本地环境配置
  • 安装启动RabbitMQ (读者自行百度)

  • 安装Nginx

  • 安装启动MySql (读者自行百度)

  • 安装启动Redis(读者自行百度)

  • 安装启动MongDB(读者自行百度)

  • 安装启动Nacos (读者自行百度)

  • 启动Sentinel(在下载目录里:java -jar sentinelxxx.jar)

  • 启动微服务顺序

    image-20220307175036322

远程部署运行说明

部署前端

采用jenkins+docker+nginx 自动部署至服务器 前端只要将代码提交至Gitee 即可自动部署到 远程服务器

参考文章

http://t.csdn.cn/bqtG1

部署后端

本地与线上环境切换

image-20220309233339090

数据库设计

课程与课程班的对应关系

image-20220305220506536

课程班与选课学生的对应关系

image-20220305221748504

选课流程

所有流程图在线预览地址:

https://www.processon.com/view/link/6223834b0e3e74108ca0a57f

密码: I84H

核心流程1: 选课预热 发布选课信息

image-20220305233013851

image-20220305233041860

image-20220305233055350

发布待选课程信息

  • 传入待选课程班id 以及 最大选课人数

  • Redis缓存待选课程信息 及可选数量

    • 这里可选数量与课程信息分开 依靠随机码相互连接

    • 通过redisson加分布式锁

      由于选课服务部署在集群,因此每个服务器都会同时将数据缓存到redis,但我们只需要让数据缓存一次。

      因此不仅需要在缓存前判断key的存在,还需要加分布式锁保证只有一条线程缓存数据

      由于这种多线程同时缓存的情况只会出现一次,因此我们不采用默认的看门狗机制(默认30秒续期 直到该方法结束),而是给锁加几秒钟过期时间就可。

      随机码如何防恶意请求:

      前端将随机码拼在选课的url上,因此单纯结合课程班id发请求是无效的。选课开始时(定时任务)后端才将随机码返回给前端,只有选课开始才能抢课。

      redis中分布式信号量的key是和课程班随机码绑定的,因此只有随机码正确才能查找到课程班对应的分布式信号量。

  • 返回发布成功即可 这个时间应该比较长 大概3~4 秒 (但只要保证学生展示待选课程从Redis中读取 毫秒级响应即可)

核心2:学生选课

image-20220306002217654

展示待选课程信息

老师立即可见

学生只有在选课时间开始时才可见

学生退选课程

解决项目中的问题

Redis

+

springboot连接远程redis出现 java.io.IOException: 远程主机强迫关闭了一个现有的连接

spring	
redis:
host:
port: 6379
password:
database: 1
timeout: 60s
## springboot2.0之后将连接池由jedis改为lettuce
lettuce:
pool:
max-idle: 30
max-active: 8
max-wait: 10000
min-idle: 10

Redis.conf

tcp-keepalive 300 # 保持长连接300s并重启Redis
  • Redis 重启后密码失效

    Redis.conf

    requirepass  密码 # 设置密码持久化 并重启Redis
  • Redis分布式锁本地连接远程失败

    Caused by: org.redisson.client.RedisResponseTimeoutException: Redis server response timeout (3000 ms) occured after 3 retry attempts.

解决:

出处

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer() // 使用单机模式
.setAddress("redis://" + redisHost + ":" + redisPort)
.setKeepAlive(true)
// 设置1秒钟ping一次来维持连接
.setPingConnectionInterval(1000)
.setPassword(redisPass);
return Redisson.create(config);
}

Nacos 配置中心不生效

导入这个依赖 原因: bootstrap.yml未生效

org.springframework.cloudspring-cloud-starter-bootstrap

quartz

springboot整合quartz遇到的错误

1、spring boot整合quartz执行多个定时任务时报:

org.quartz.ObjectAlreadyExistsException: Unable to store Job : ‘group1.job1’, because one already exists with this identification.

解决办法: 在初始化调度的时候clean一下: scheduler.clear();

项目 高并发本地测压

创建1000个测试用户并生成各自的token 存入user.txt文件等待测试

@GetMapping("/batchInsertUser")  //@RequestParam(value = "date")Integer date,@RequestParam("size")Integer size
public BaseResponse batchInsertUser() throws Exception {
int firstId =3110000;
int size=1000;
String username = firstId+"";
String password = "123456";
permissionService.insertFirstUser(firstId,username,password);


// 2203110000
for (int i = 1; i < size; i++) {
int newId = firstId+i;
String uname = newId+"";
log.info(uname+"---"+i);
permissionService.insertUser(uname,password);
}

File file =new File("D:/Users/Administrator/Desktop/user.txt");
if(file.exists())
{
file.delete();
}
file.createNewFile();
BufferedWriter out = new BufferedWriter(new FileWriter(file));

for (int i=0;i<size;i++)
{
JWTInfo jwtInfo = new JWTInfo(firstId+i + "" ,firstId+i + "",firstId+i + "");
// 这里生成token 并派发
String token = jwtTokenUtil.generateToken(jwtInfo);
String myId = firstId+i + "";
//JWT记录写入Redis
writeOnlineLog(jwtInfo);
out.write(myId+","+token+"\r\n"); // \r\n即为换行
}
out.flush(); // 把缓存区内容压入文件
out.close();


return new BaseResponse(200,"插入成功");
}

创建线程组 1000个线程 3次循环

image-20220311204044965

user.txt文件导入测试

image-20220311204214954

image-20220311204257614

清除缓存预热

image-20220311205549089

选课预热 一门课 限选60人 (1000人并发访问)

image-20220311205648150

查看选课状态

image-20220311210959030

启动测试

查看结果树与报告

image-20220311212511192

image-20220311212540352

image-20220311212625289

判断学生是否重复选课

SELECT * FROM
(SELECT `student_id`,COUNT(*) AS c
FROM st_courseclass_student GROUP BY `student_id`) t WHERE c>1;