由于选课时间集中, 在同一时间进入系统抢占有限的资源, 导致系统服务响应速度明显下降, 严重时甚至会造成服务器崩溃。这种问题在目前实行学分制的国内高校中普遍存在。当系统软件不具备高并发性时,就无法顺畅承接超大流量,当请求过多,系统就会直接崩溃。
本小组致力于针对原有选课系统的缺点,利用高并发技术的方法论以及设计原则,结合业务本身进行架构设计,以应对系统面临的流量冲击。从网络、架构、数据库等多方面进行系统优化,从而降低系统的响应时间,提高系统吞吐量,为学生提供一个高可用的选课系统,以达到系统能够在高并发下平稳处理大流量且自身依然运行良好的目的,让学生不再受系统崩溃所困扰。
选课系统的特点
1.业务特点:在选课开始之前,流量一直是很平稳的状态;当选课刚刚开始时,系统流量呈直线突增;在选课活动开始一段时间或结束之后,流量又会急速下落。
2.技术特点:
- 瞬时并发量高。因为课程容量限制的特性,决定了一旦开始“抢课”,热门的课程就会出现流量洪峰。
- 并发读写。读比写要多,属于多读写少的场景。课程查询页访问大,但是真正选课成功的不多,即查询的流量要远大于扣减容量的流量。
对以上系统特点进行分析,得到以下设计原则:
系统基于Spring、SpringMVC、Mybatis框架,采用MVC开发模式,将系统分为模型层、视图层、控制层三层。模型层Model主要负责处理业务逻辑以及数据库的交互,视图层View主要负责显示数据和提交数据,控制层Controller主要是用作辅助捕获请求并控制请求转发。
同时从业务逻辑上来看,系统按照三层架构进行软件包的归类,分为控制层Controller、业务层Service和持久层Dao,让架构更加清晰。
包结构大致如下:
problem-provider
|--annotation # 自定义注解
|--aop # 切面类 —— SpringAOP的应用
|--common # 通用类
| |--config # 配置类
| |--constant # 常量类
| |--enums # 枚举类
| |--util # 工具类
|--controller # 控制层 —— Controller层的体现
|--dto # 数据传输对象
|--entity # 实体类 —— MyBatis中实体类的定义
|--event # 事件 —— 观察者设计模式
|--filter # 过滤器 —— SpringMVC实现请求的拦截过滤
|--handle # 处理器
|--listener # 监听器
|--mapper # 持久层 —— dao层的体现
|--service # 业务层 —— 接口设计思想体现
| |--impl # 实现类 —— Service层的体现
同时本系统通过SpringBoot对SSM进行整合,约定大于配置,避免了定义大量配置文件的繁琐,也一定程度的解决版本依赖冲突等问题。只需要在资源目录下配置application.yml文件配置数据库,redis,rabbitmq等服务的连接信息以及mapper文件即可。
水平扩展有多种方式,由于DNS不会感知到具体WEB站点的可用性,当某个WEB站点服务故障(如网络中断、系统崩溃等),用户对这个站点的访问将失败,故不通过DNS进行水平扩展。
考虑到Nginx是一个高性能的 HTTP 和反向代理服务器,反向代理可以实现隐藏服务器的内部结构,集成防火墙来防御外界DDOS攻击,通过负载均衡分配流量到不同服务器上等功能。此外Nginx支持对故障 WEB 站点的探测感知,能够把流量引导到非故障的WEB站点,同时也没有DNS 负载均衡模式下暴露过多IP、扩容非实时等问题。
为避免反向代理层的单点故障风险,采用两台 Nginx 组成一个集群,分别部署 Keepalived,设置成相同的虚 IP,实现 Nginx 负载均衡服务的高可用。当一台 Nginx 挂了, Keepalived 能够探测到并将流量自动迁移到另一台 Nginx 上。该方案解决了反向代理层的高可用问题,但是 Nginx 集群的资源利用率下降到 50%(一主一备)。对于院校选课活动,每秒的HTTP请求峰值一般在 10 万以下,该架构的 WEB 站点响应能力应能满足要求,最多再适当提高 Nginx 服务器的配置应能解决问题。故系统最终决定采用基于Nginx集群反向代理的方式对WEB站点层进行水平扩展。
班级相关实体
学生相关实体
课程相关实体
系统相关实体
实体联系图
实体间关系分析
- a:
- 一个院包含多个系
- 一个系包含多个专业
- 一个专业包含多个班
- 一个班包含多个学生
- 一个班拥有一个方案内选课
- b:
- 一个学生属于一个班
- 一个学生拥有多个学期的学分
- 一个学生拥有多个学期的选课
- c:
- 一门课有多个课程班
- 一门课有多门先修课
- 一门课有多个开课的时间和地点
- 一门课可以设置多个应急条件
- d:
- 一个学期可以有多个选课轮次
- 一个学期可以选择多个课程班
- 一个学期每个班级有一个推荐班选课
- e:
- 一个用户可以收到多个消息与通知
- 学生的每次请求都会被记录在日志表
- 管理员每次请求都会被记录在日志表
字段规定:
- 小数类型使用decimal、禁止使用float和double类型,时间采用date_time类型
- 绝大多数表必备三个字段:id、create_time以及update_time,部分表的数据采用逻辑删除
SQL语句:
- 禁止使用存储过程,存储过程难以调试和扩展,且没有移植性
- 不得使用外键(影响插入速度)和级联(更新强阻塞,存在更新风暴风险),一切外键概念在应用层解决
安全性问题:
- ORM映射时参数使用#{}而非${},以防止SQL注入问题
- 用户密码先加盐处理后再进行MD5加密存储,以提高密码复杂程度和防御彩虹表攻击
使用DataGrip数据库设计工具,根据上述实体属性以及联系进行数据库以及表的创建。
以下为学生表创建示例:
对应建表语句如下
create table scc_choose_round
(
id int not null
primary key,
semester int null,
round_no tinyint null,
start_time datetime null,
end_time datetime null,
tips varchar(255) null,
create_time datetime null,
update_time datetime null
)
comment '选课轮次表';
create table scc_class
(
id int not null
primary key,
college_name varchar(20) null,
department_name varchar(20) null,
subject_name varchar(20) null,
year smallint null,
class_no tinyint null,
class_name varchar(20) null,
graduation_credits smallint null,
create_time datetime null,
update_time datetime null
)
comment '班级表';
create table scc_class_course
(
id bigint not null
primary key,
class_id int null,
course_id int null,
commended_time int null,
is_must tinyint null,
is_deleted tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '方案内课程表';
create table scc_college
(
id smallint not null
primary key,
college_name varchar(20) null
)
comment '学院表';
create table scc_course
(
id char(10) not null
primary key,
course_name varchar(30) null,
campus varchar(20) null,
college varchar(20) null,
type varchar(20) null,
general_type varchar(20) null,
credit decimal(10, 1) null,
class_num tinyint null,
create_time datetime null,
update_time datetime null
)
comment '课程表';
create table scc_course_class
(
id bigint not null
primary key,
course_id char(10) null,
is_mooc tinyint null,
language varchar(20) null,
choosing_num int null,
capacity int null,
teacher varchar(100) null,
exam_type varchar(20) null,
exam_time varchar(50) null,
is_deleted tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '课程班表';
create table scc_course_dependence
(
id bigint not null
primary key,
course_id int null,
pre_course_id int null,
is_deleted tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '课程依赖表';
create table scc_course_emergency
(
id bigint not null
primary key,
course_id char(10) null,
only_to_class int null,
only_to_grade int null,
is_deleted tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '课程紧急设置表';
create table scc_course_timeplace
(
id bigint not null
primary key,
course_class_id bigint null,
duration_time varchar(20) null,
week_day tinyint null,
day_no varchar(20) null,
place varchar(50) null,
is_deleted tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '课程时间地点表';
create table scc_department
(
id smallint not null
primary key,
college_id smallint null,
department_name varchar(20) null
)
comment '系表';
create table scc_student
(
id bigint not null
primary key,
name varchar(50) null,
gender tinyint null,
class_id int null,
email varchar(100) null,
phone char(11) null,
id_card char(18) null,
password char(32) null,
salt char(32) null,
status tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '学生表';
create table scc_student_course
(
id bigint not null
primary key,
student_id int null,
course_class_id bigint null,
semester int null,
credits decimal null,
is_deleted tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '学生选课表';
create table scc_student_credits
(
id bigint not null
primary key,
student_id bigint null,
semester int null,
max_subject_credit decimal(10, 1) null,
choose_subject_credit decimal(10, 1) null,
create_time datetime null,
update_time datetime null
)
comment '学生学分表';
create table scc_subject
(
id smallint not null
primary key,
department_id smallint null,
subject_name varchar(20) null
)
comment '专业表';
create table scc_sys_backend_log
(
id bigint not null
primary key,
type tinyint null,
request_ip varchar(255) null,
manager_id int null,
request_api varchar(255) null,
request_body text null,
response_body text null,
create_time datetime null
)
comment '后台日志表';
create table scc_sys_frontend_log
(
id bigint not null
primary key,
type tinyint null,
request_ip varchar(255) null,
student_id int null,
request_api varchar(255) null,
request_body text null,
response_body text null,
create_time datetime null
)
comment '前台日志表';
create table scc_sys_manager
(
id int not null
primary key,
manager_name varchar(20) null,
type smallint null,
password char(32) null,
salt char(32) null,
mobile_phone varchar(20) null,
last_login datetime null,
is_deleted tinyint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '系统管理员表';
create table scc_sys_notice
(
id bigint not null
primary key,
student_id bigint null,
message varchar(255) null,
status smallint default 0 null,
create_time datetime null,
update_time datetime null
)
comment '系统通知表';
经过前面的设计,WEB 站点层的高性能、高可用都得到了一定的保证,但此时数据库IO操作很可能成为了瓶颈。 数据层同样需要进行改进,数据层库架构优化的目的同样是实现高性能、高可用。
优化方案:
**问题:**当请求的QPS达到一定的水平后,系统将面临性能降低(并发越大查询速率越低)以及并发下降(当请求数超过正常单机数据库所能抗住的QPS时,数据库将会卡顿甚至宕机)。
优化方案:引入缓存
使用缓存方案可以提高访问性能、降低网络拥堵,减轻服务负载等。缓存读写性能高,且预热快。将要被请求的数据放入缓存中,请求将先在缓存中获取数据(单级缓存可承载的并发量可能是MySQL的几十倍)。
引入缓存数据库Redis设计合适的缓存淘汰策略,将热点数据存入Redis方便后续读取。
Todo:引入Redis集群,解决数据一致性问题,预防缓存雪崩与缓存穿透问题,以搭建高可用的Redis集群。
如果每次请求都需要新建连接,即TCP需要经历“三次握手”,这个过程很费时间。另外一旦建立连接则还需要关闭连接,即TCP需要经历“四次挥手”,又需要时间开销。
问题:
优化方案:引入池化技术
可以用数据库连接池来优化数据库的连接,这就是连接池技术。即预先分配一批并将他们放入一个缓存区中循环使用,形成池化效应。
**问题:**由于选课系统中每个人的目标课程不同,所以缓存命中率就没有那么高,会有很大的流量被转发到MySQL数据库中,MySQL的压力也随之增大,单台MySQL实例将面临无法满足当前的业务需求。
优化方案:引入读写分离
针对于选课这种并发量大的读请求,最直接的便是搭建多台MySQL实例分摊读请求。将读流量分摊到各个从库中,而主库负责写流量,通过Binlog实现数据同步。
解决数据一致性问题
做好幂等性设计(所谓幂等性即指用户对于同一个操作发起一次请求或多次请求,得到的结果都是一样的,主要用在重复请求上)。由于并发量大的问题,考虑采用乐观锁而非悲观锁以得到更好的并发访问性能。
引入搜索引擎
每次请求都需要建立和关闭连接,会增加请求延迟
优化方案:引入池化技术
可以用数据库连接池来优化数据库的连接,这就是连接池技术。即预先分配一批并将他们放入一个缓存区中循环使用,形成池化效应。
**问题:**由于选课系统中每个人的目标课程不同,所以缓存命中率就没有那么高,会有很大的流量被转发到MySQL数据库中,MySQL的压力也随之增大,单台MySQL实例将面临无法满足当前的业务需求。
优化方案:引入读写分离
针对于选课这种并发量大的读请求,最直接的便是搭建多台MySQL实例分摊读请求。将读流量分摊到各个从库中,而主库负责写流量,通过Binlog实现数据同步。
解决数据一致性问题
做好幂等性设计(所谓幂等性即指用户对于同一个操作发起一次请求或多次请求,得到的结果都是一样的,主要用在重复请求上)。由于并发量大的问题,考虑采用乐观锁而非悲观锁以得到更好的并发访问性能。
引入搜索引擎
对于海量数据的搜索,SQL语句查询时间过长,且不能很好的命中索引。对于like这类模糊查询,只有后模糊匹配语句才能命中索引,一旦没有命中索引,进行全表扫描则性能很低。故后续考虑引入ElasticSearch搜索引擎。
项目开源地址:
https://gitee.com/Ken-Chy129/
https://github.com/kil1ua/