设计千万级学生管理系统的考试试卷存储方案
一:容量预估
单个学校课程数量分析:以清华大学为例,共有专业 82 个,由于这类好的大学专业开设多,一般学校或者例如师范等专业类学校学科较少,因此预估平均学科 70 个,假设每个学科每学期开展 20 门课程,有可能存在公共学科,假设部分公共学科是由同一个教师授课和出试题,平均下来每学科每学期有 15 门课,那么一个学校的课程总数为:4(年级数)x 2(学期数)x 70(专业)x 15(课程数)=8400 门课
学校数量分析:该案例是千万学生管理系统,同样以清华大学为例,在校学生 53302 人,由于人数也会存在差异比较大,但是由于扩招,现在大部分大学一年新生人数都在 1 万左右,因此预测每个学校平均在校学生为 45000 人。该系统为千万学生管理系统,假设现在总人数为 2000 万学生,那么学校数量约为 450 所。
单张试卷容量分析:每门考试的答案 20 判断题、20 选择题、4 道大题,预估一道判断题 100 个汉字(符号算在内),一道选择题,题目为 100 个汉字,一道题有 4 个答案,每个答案 50 个汉字,每道大题有 500 个汉字,那么一套试卷所占空间为:(20 x 100 + 20 x(100+4 x 50) + 4 x 500) x 2Byte = 20000Byte 约为 10K。
总是卷容量,450(学校数量)* 8400(单个学校课程数量)*10K(单张试卷所占容量) 约等于 36G。
二、数据结构
1、数据存储读取分析
首先是对于试卷内容的读写场景分析:
写:老师在不同题目类型中录入相应的试题,也有可能对录入好的实体进行修改,重新排序等。
读:学生在考试开始时全量读取试卷内容,也可以分题型多次读取试卷内容,也可以每次只读取一条数据。
如果是一次全量读取试卷内容,在不考虑 Redis 数据压缩的情况下,Value 为整个试卷内容,内容为 10K,在 QPS 为 5W 的情况下,每秒传输的 Value 所占带宽约为 500M。
如果按照试卷类型读取的话,按照上面各类题型存储占用分析,选择题占判断题占 2K,选择题占 6K,大题占 2K。实际场景有人做的快有人做得慢,在最开始请求选择题时,QPS 肯定是 5W,且都是请求的判断题,这时的带宽占用约为 100M;在后续过程中,请求会分散,QPS 按照 2W 预估,那么对于选择题请求的带宽占用约为 100M。
如果按照单条试题访问的话,QPS 最高的是第一道判断题,QPS 5W,内存占用 200Byte,传输带宽约为 5M,占用字数最多的是大题 1000Byte,但是如上分析,QPS 预估为 2W,也是约为 5M。
综上分析,使用单条数据读取的方式对于网络带宽的占用最低,也满足读写业务需求,同时考虑对于数据的压缩,可能更进一步的减少带宽。
2、数据结构分析
数据结构选择分析
采用 String 数据结构:
key 组成逻辑:学校-院系-专业-课程-类型-题号
value:采用 String 数据结构
采用 List 数据结构:
可以全量或者按题型存储,单条查询不太满足。
采用 Map 数据结构:
key 组成逻辑:学校-院系-专业-课程-题型,filed 为题目序号
value 为试卷内容,这样带宽占用和 List 一样;
采用 Set 数据结构:
不满足业务场景
采用 Scored Set 数据结构:
key 组成逻辑:学校-院系-专业-课程,为了满足顺序的频繁调整,分值为添加的顺序乘以 20
value:题目内容
3、数据结构选择
结论:选择 Scored Set。
分析:根据上面分析,使用 String 和 Scored Set 都是可以的,但是如果使用 String,从业务上说,对于调整顺序会比较麻烦,例如把最后一个调整到第一个,可能需要操作所有数据,另外从存储空间占用和查询性能上来说,分散使用 String 存储,会占用更大的空间,查询性能对比与 Scored Set 肯定是会慢一点;而 Scored Set 从业务上来说,调整顺序可以直接调整分数即可,比较简单,且从存储空间占用和查询性能上来说,也会更优。
三、读写流程
新增试题:教师新增一道题,使用根据其所在学校、院系、专业、课程组装 Key,添加题目时,使用 zrevrange key 0 0 withscores 先获取最大分值,使用 ZADD key score member 新增一条试题,score 是原最大 score+20,为后面调整顺序做铺垫,这一步涉及两个操作,可以使用 Redis 事务或者使用 lua 脚本保证原子性。
删除试题:使用 zremrangebyrank key index index 删除指定顺序的题目
修改试题:使用 zrevrange key index index withscores 先获取需要删除的数据和分值,使用 zremrangebyrank key index index 删除指定顺序的题目,然后使用 ZADD key score member,其中 score 还是原值,这里也涉及两个操作,可以使用 Redis 事务或者使用 lua 脚本保证原子性。
调整试题顺序:确定好该条数据调整的位置后,使用 zrevrange key index index withscores 先获取需要原来该数据的 score,使用 zremrangebyrank key index index 删除指定顺序的题目,然后使用 ZADD key score member,其中 score 是调整前指定位置的 score+1,这里也涉及两个操作,可以使用 Redis 事务或者使用 lua 脚本保证原子性。 举例:总共有 10 条,每条间隔数为 20,当前试题在第 2 道,要调整到第 6 道,那么首先查询出第 6 道题的 score,假设为 120,然后删除第二道题,然后再插入改题目,score 为 121,这样不需要调整其他题目。
学生读取具体题目:学生按照学校-院系-专业-课程作为 Key,使用 zrange key 0 0 进行查询查询第一道题,后面的题,是顺序还是跳着做都可以,只要选择题号后,使用 zrange key index index 获取即可
学生读取全量数据展示(学生可以选择题号进行切换):题目数量固定,直接展示写死展示即可。如果是每门课程试题数量不定,就需要使用 zcard 获取题目数量进行展示,如果要展示更详细的每种题型多少道,可以在写入时 key 加类型或者的方式处理,再或者根据不同的题型,对于 score 值有区间限制,对于不同区间获取总数展示。
四、Sentinel 集群选择
试卷请求 QPS 为 5 万/s,Redis 单机读取性能为 11W,因此单机就可以满足读取性能要求,为了保证高可用和自动切换,使用 Sentinel,为了保证出现出从切换仍然是高可用,可以设计整个 Redis 集群为 3 台服务器,一主两从,同时为了保证 Sentinel 的高可用,也是用三台服务器组成 Sentinel 集群。
评论