CTF平台实时榜单功能后端设计方案
最近复盘项目的时候,把CTF平台中实时榜单的后端设计部分整理了一下,在此分享出来。
文章目录
1.CTF平台实时榜单功能介绍(需求分析)
线上CTF竞赛(夺旗赛)通常是在限定的时间内访问题目,解出flag后在平台上提交,提交后能够获得一定的分数,最后会按总分进行排序确定此次竞赛的成绩。
在CTF竞赛中,榜单功能是访问量最高的一个功能,通过查看榜单的排名,做题情况,能够感知题目的难度,方便及时调整团队做题的策略。这个榜单通常需要反映全部战队的一个做题情况,包括每个题的解出情况,大致如下图(截图来自i春秋,第二届“祥云杯”网络安全大赛暨吉林省第四届大学生网络安全大赛)。
大致会有如下功能:
- 1.整体按总分进行排序。
- 2.对于每一行,除了显示战队的基础信息,总分之外,还会显示每一个题目的解出情况,包括是否一二三血。
- 3.实时榜单,有任何题目解出情况,题目变动情况(增删题目),总榜单都会实时变动。
实时榜单功能作为整个CTF平台最重要的功能之一,同时也是访问量最高的一个功能,对这个功能做一个比较全面的设计是很有必要的。
2.实时榜单具体功能点特点分析
对实时榜单整个接口来说,一般具体以下特点:
- 1.接口访问特别频繁,特别是在比赛快结束的时候,访问量几乎会翻几倍。
- 2.比赛过程中,除了极少数签到题,一般的题目不会集中在一个时间段内提交,实际解题成功的数量在总队伍数里面占比较少。
- 3.一般CTF竞赛会分批次的上题,也就是说:并不是比赛开始后,题目就不再变动了的。但是,上题次数不多,一般集中上题1-3次。
- 4.一般CTF题目的数量不会太多,总题目一般就20-50道左右。
- 5.一般参赛队伍数不会太多,一般参赛队伍一般在100-1500左右。
- 6.排名是实时会变动的。
3.难点分析
从上面的具体情况可以发现,其实整体的排序计算开销是非常小的,因为队伍数和题目数都非常少,主要的开销集中在数据库IO,而榜单接口的访问并发非常大,如果每次访问的请求最终都打到数据库上,很有可能造成数据库宕机。
如果简单的对榜单进行缓存,那么只要有人交题,就会导致缓存失效。这个时候还是有可能导致数据库宕机。
因此我们要设计好对榜单的缓存机制,尽量少访问数据库。
4.整体设计思路
通过上面的分析,我们设计榜单的关键就在如何在整个榜单维护在redis缓存中,从而降低数据库的查询次数。
在比赛期间,我们完全可以将整个榜单全部缓存到redis中,当比赛结束时,如果再次查询榜单(这个时候这个接口的访问量已经非常少了),我们可以从数据库中查出并将整个榜单存储到数据库中(因为此时整个榜单已经不会再变动)。
我们如果要保证缓存中的榜单总是实时正确的,就需要在有任何队伍分数发生变化的时候或者新增题目的时候,重新生成整份榜单并存回redis中,注意,这个时候,我们不需要从数据库中查询记录,只需要把提交记录保存到数据库,并且重新生成缓存即可。
除此之外,我们还需要注意一个问题,因为很有可能会出现多个线程对redis完成读,更新,写入的一个事务,这其中是存在线程安全问题的,针对上述提交正确次数不会过于集中的情况,我们可以对每个更新事务加锁来解决线程安全的问题。
总结一下:整体设计思路是:比赛过程中把榜单维护在redis中,比赛结束后,再将整份榜单记录写入到数据库。
5.部分涉及的实体类介绍
以下实体类我只将与榜单功能相关的字段写出来了,实际情况可以再添加更多的字段。
5.1 Team类
存储团队信息的实体类,和数据库字段一一对应。
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/12 16:48
* @description
*/
public class Team {
// 团队名称
private String teamName;
// 团队id
private String id;
// 公司或学校名称
private String belongName;
// 组队性质
private String companyOrSchool;
public String getTeamName() {
return teamName;
}
public void setTeamName(String teamName) {
this.teamName = teamName;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getBelongName() {
return belongName;
}
public void setBelongName(String belongName) {
this.belongName = belongName;
}
public String getCompanyOrSchool() {
return companyOrSchool;
}
public void setCompanyOrSchool(String companyOrSchool) {
this.companyOrSchool = companyOrSchool;
}
@Override
public String toString() {
return "Team{" +
"teamName='" + teamName + ''' +
", id='" + id + ''' +
", belongName='" + belongName + ''' +
", companyOrSchool='" + companyOrSchool + ''' +
'}';
}
}
5.2 Problem类
题目的实体类,和数据库字段一一对应。
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/12 16:49
* @description
*/
public class Problem {
// 题目id
private String problemId;
// 题目名称
private String problemName;
// 题目类型
private String tag;
// 题目分数
private Double score;
public String getProblemId() {
return problemId;
}
public void setProblemId(String problemId) {
this.problemId = problemId;
}
public String getProblemName() {
return problemName;
}
public void setProblemName(String problemName) {
this.problemName = problemName;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public Double getScore() {
return score;
}
public void setScore(Double score) {
this.score = score;
}
@Override
public String toString() {
return "Preoblem{" +
"problemId='" + problemId + ''' +
", problemName='" + problemName + ''' +
", tag='" + tag + ''' +
", score=" + score +
'}';
}
}
5.3 ProblemSolveRecord
解题记录,和数据库字段一一对应,在使用的时候,是筛选出了所有提交正确的记录。
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/12 18:21
* @description 成功解题记录
*/
public class ProblemSolveRecord {
// 题目id
private String problemId;
// 解出团队
private String teamId;
// 解出人
private String userId;
// 题目名称
private String problemName;
// 题目类型
private String tag;
// 题目得分
private Double getScore;
// 是否一血
private Boolean firstBlood;
// 是否二血
private Boolean secondBlood;
// 是否三血
private Boolean tribleBlood;
public String getProblemId() {
return problemId;
}
public void setProblemId(String problemId) {
this.problemId = problemId;
}
public String getTeamId() {
return teamId;
}
public void setTeamId(String teamId) {
this.teamId = teamId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getProblemName() {
return problemName;
}
public void setProblemName(String problemName) {
this.problemName = problemName;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public Double getGetScore() {
return getScore;
}
public void setGetScore(Double getScore) {
this.getScore = getScore;
}
public Boolean getFirstBlood() {
return firstBlood;
}
public void setFirstBlood(Boolean firstBlood) {
this.firstBlood = firstBlood;
}
public Boolean getSecondBlood() {
return secondBlood;
}
public void setSecondBlood(Boolean secondBlood) {
this.secondBlood = secondBlood;
}
public Boolean getTribleBlood() {
return tribleBlood;
}
public void setTribleBlood(Boolean tribleBlood) {
this.tribleBlood = tribleBlood;
}
}
5.4 ProblemSolve
不是数据库中表对应的实体类,代表对某个队伍来说,具体某个题目的解题情况,用于榜单每一行的题目显示,相关字段可以从上述ProblemSolveRecord对象中拷贝。
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/11 22:11
* @description 具体题目情况
*/
public class ProblemSolve {
// 题目id
private String problemId;
// 题目名称
private String problemName;
// 题目类型
private String tag;
// 题目得分
private Double getScore;
// 是否一血
private Boolean firstBlood;
// 是否二血
private Boolean secondBlood;
// 是否三血
private Boolean tribleBlood;
public ProblemSolve(String problemId, String problemName, String tag) {
this.problemId = problemId;
this.problemName = problemName;
this.tag = tag;
}
public String getProblemId() {
return problemId;
}
public void setProblemId(String problemId) {
this.problemId = problemId;
}
public String getProblemName() {
return problemName;
}
public void setProblemName(String problemName) {
this.problemName = problemName;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
public Double getGetScore() {
return getScore;
}
public void setGetScore(Double getScore) {
this.getScore = getScore;
}
public Boolean getFirstBlood() {
return firstBlood;
}
public void setFirstBlood(Boolean firstBlood) {
this.firstBlood = firstBlood;
}
public Boolean getSecondBlood() {
return secondBlood;
}
public void setSecondBlood(Boolean secondBlood) {
this.secondBlood = secondBlood;
}
public Boolean getTribleBlood() {
return tribleBlood;
}
public void setTribleBlood(Boolean tribleBlood) {
this.tribleBlood = tribleBlood;
}
}
5.5 OneTotalRecord
代表榜单中的一行记录,包含了一行需要显示的所有记录字段。其中对于这个团队来说的所有解题情况,都可以靠titles集合来遍历。
注意这里将成功解题的方法添加到了实体类中。
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/11 21:53
* @description 总榜单条数据
*/
public class OneTotalRecord {
// 团队名称
private String teamName;
// 团队id
private String id;
// 公司或学校名称
private String belongName;
// 组队性质
private String companyOrSchool;
// 团队总解题数
private Integer totalSolveNum;
// 团队总分
private Double totalScore;
// 一血数
private Integer firstBlood;
// 最后一次成功提交时间
private Date lastSolveTime;
// 所有解题数据 具体的分类由前端进行分类
private List<ProblemSolve> titles = new LinkedList<>();
public OneTotalRecord(){
}
public OneTotalRecord(String teamName, String id, String belongName, String companyOrSchool){
}
public String getTeamName() {
return teamName;
}
public void setTeamName(String teamName) {
this.teamName = teamName;
}
public String getBelongName() {
return belongName;
}
public void setBelongName(String belongName) {
this.belongName = belongName;
}
public String getCompanyOrSchool() {
return companyOrSchool;
}
public void setCompanyOrSchool(String companyOrSchool) {
this.companyOrSchool = companyOrSchool;
}
public Integer getTotalSolveNum() {
return totalSolveNum;
}
public void setTotalSolveNum(Integer totalSolveNum) {
this.totalSolveNum = totalSolveNum;
}
public Double getTotalScore() {
return totalScore;
}
public void setTotalScore(Double totalScore) {
this.totalScore = totalScore;
}
public Integer getFirstBlood() {
return firstBlood;
}
public void setFirstBlood(Integer firstBlood) {
this.firstBlood = firstBlood;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Date getLastSolveTime() {
return lastSolveTime;
}
public void setLastSolve(Date lastSolve) {
this.lastSolveTime = lastSolve;
}
public List<ProblemSolve> getTitles() {
return titles;
}
public void setTitles(List<ProblemSolve> titles) {
this.titles = titles;
}
public void solveOneProblem(ProblemSolve solve){
for(ProblemSolve problemSolve : titles){
if(problemSolve.getProblemId().equals(solve.getProblemId())){
problemSolve.setGetScore(solve.getGetScore());
problemSolve.setFirstBlood(solve.getFirstBlood());
problemSolve.setSecondBlood(solve.getSecondBlood());
problemSolve.setTribleBlood(solve.getTribleBlood());
totalScore += solve.getGetScore();
break;
}
}
}
@Override
public String toString() {
return "OneTotalRecord{" +
"teamName='" + teamName + ''' +
", id='" + id + ''' +
", belongName='" + belongName + ''' +
", companyOrSchool='" + companyOrSchool + ''' +
", totalSolveNum=" + totalSolveNum +
", totalScore=" + totalScore +
", firstBlood=" + firstBlood +
", titles=" + titles +
'}';
}
}
6.具体业务功能ServeiceImpl设计
针对上述需求分析,我们的榜单的Service接口只需要三个public方法即可。
- 1.
public List<OneTotalRecord> getAllRecord(String competitionId)
获取指定比赛的榜单。 - 2.
public void addOneSolution(String competitionId, String teamId, ProblemSolve problemSolve)
新增一条成功解题记录时,更新榜单。 - 3.
public void addOneProblem(String competitionId, Problem problem)
新增一个题目的时候,更新逻辑。(可以在批量上题完毕后调用一次)
这里没有给出删除题目的方法,因为比赛过程中删除题目会有很多种不同的处理逻辑,需要自行去实现处理逻辑。
6.1 获取榜单方法 getAllRecord
总体思路就是,先从redis中获取,redis中有直接返回,没有就先判断一下是否是初始化榜单(没有任何成功解题记录就是初始化榜单),如果不是初始化(说明此时缓存失效,需要从数据库中查出),然后获得操作榜单的锁,开始从数据库中查出记录,生成整个榜单。
public List<OneTotalRecord> getAllRecord(String competitionId){
// 1.从redis中查询,如果存在,直接反序列化成对象后返回
List<OneTotalRecord> redisRecords = getRecordsAllRedis(competitionId);
if(redisRecords != null){
return redisRecords;
}
// 如果redis中不存在
// 2.数据库查询出当前比赛是否有成功的解题记录 如果没有,进入初始化流程,初始化后直接返回
if(checkValidSolve(competitionId)){
return initRecord(competitionId);
}
lock.lock();
try{
// 某个线程获得锁时已经成功初始化
redisRecords = getRecordsAllRedis(competitionId);
if(redisRecords != null){
return redisRecords;
}
// 此时数据库中有对应的解题记录,但是redis中没有缓存,很有可能是缓存失效 那么需要从数据库中查出所有解题记录,生成榜单
// 3.查询对应榜单
List<ProblemSolveRecord> solveRecords = getCompetitionSolveRecord(competitionId);
// 将榜单列表转化为map 键为团队id 值为解题列表
Map<String, List<ProblemSolveRecord>> solveMap = new HashMap<>();
for(ProblemSolveRecord solveRecord : solveRecords){
if(solveMap.containsKey(solveRecord.getTeamId())){
solveMap.get(solveRecord.getTeamId()).add(solveRecord);
}else{
solveMap.put(solveRecord.getTeamId(), new ArrayList<ProblemSolveRecord>());
solveMap.get(solveRecord.getTeamId()).add(solveRecord);
}
}
// 4.查询出所有团队
List<Team> teams = getAllCompetitionTeam(competitionId);
if(teams.size() == 0){
return null;
}
// 5.查询出所有的题目
List<Problem> problems = getAllCompetitionProblem(competitionId);
// 6.初始化每一条记录 数量固定,使用ArrayList
List<OneTotalRecord> records = new ArrayList<>(teams.size());
for(Team team : teams){
// 初始化基本信息
OneTotalRecord record = new OneTotalRecord(team.getTeamName(), team.getId(), team.getBelongName(), team.getCompanyOrSchool());
List<ProblemSolve> solveProblemList = record.getTitles();
// 初始化题目信息
for(Problem problem : problems){
// 初始化解题记录
solveProblemList.add(new ProblemSolve(problem.getProblemId(), problem.getProblemName(), problem.getTag()));
}
if(solveMap.containsKey(team.getId())){
List<ProblemSolveRecord> solveRecordList = solveMap.get(team.getId());
// 遍历当前团队解出的每一个题
for(int i = 0; i < solveRecordList.size(); i++){
ProblemSolveRecord solveRecord = solveRecordList.get(i);
String problemId = solveRecord.getProblemId();
// 寻找每一个题目对应的记录
for(ProblemSolve problemSolve : solveProblemList){
// 找到了对应的解题记录
if(problemSolve.getProblemId().equals(problemId)){
problemSolve.setGetScore(solveRecord.getGetScore());
problemSolve.setFirstBlood(solveRecord.getFirstBlood());
problemSolve.setSecondBlood(solveRecord.getSecondBlood());
problemSolve.setTribleBlood(solveRecord.getTribleBlood());
break;
}
}
}
solveMap.remove(team.getId());
}
}
// 7.排序
recordsSortByScore(records);
// 8.存入redis
saveRecordsToRedis(records);
return records;
}finally {
lock.unlock();
}
}
6.2 新增解题记录后调用 addOneSolution
新增成功解题记录时,只需要获取锁,然后更新对应的记录,最后对榜单重新排序即可。
/**
* 当新增一条解题记录时的处理逻辑
* @param competitionId
* @param teamId
* @param
*/
public void addOneSolution(String competitionId, String teamId, ProblemSolve problemSolve){
lock.lock();
try{
// 1.获取之前的榜单
List<OneTotalRecord> records = getAllRecord(competitionId);
// 2.找到指定团队
for(OneTotalRecord record : records){
if(record.getId().equals(teamId)){
// 新增解题
record.solveOneProblem(problemSolve);
break;
}
}
// 3.重新排序
recordsSortByScore(records);
// 4.存入redis
saveRecordsToRedis(records);
}finally {
lock.unlock();
}
}
6.3 新上题后调用 addOneProblem
新上题时,获取锁后,往每一条拌榜单记录里新增一条初始题目记录即可。
/**
* 后台新上了题目
* @param problem
*/
public void addOneProblem(String competitionId, Problem problem){
lock.lock();
try{
// 1.获取之前的榜单
List<OneTotalRecord> records = getAllRecord(competitionId);
// 2.给每条数据新增一个题目
for(OneTotalRecord record : records){
record.getTitles().add(new ProblemSolve(problem.getProblemId(), problem.getProblemName(), problem.getTag()));
}
// 3.无需重新排序 直接存入redis
saveRecordsToRedis(records);
}finally {
lock.unlock();
}
}
6.4 完整service
将原本项目中的serviceImpl的关键方法抽离出来重新处理了一下,只需要按照需求拓展实体类,重写这些方法就可以放在项目中使用。
package awardList;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author ATFWUS
* @version 1.0
* @date 2021/11/12 16:29
* @description
*/
public class RecordServiceImpl {
private final ReentrantLock lock = new ReentrantLock();
public List<OneTotalRecord> getAllRecord(String competitionId){
// 1.从redis中查询,如果存在,直接反序列化成对象后返回
List<OneTotalRecord> redisRecords = getRecordsAllRedis(competitionId);
if(redisRecords != null){
return redisRecords;
}
// 如果redis中不存在
// 2.数据库查询出当前比赛是否有成功的解题记录 如果没有,进入初始化流程,初始化后直接返回
if(checkValidSolve(competitionId)){
return initRecord(competitionId);
}
lock.lock();
try{
// 某个线程获得锁时已经成功初始化
redisRecords = getRecordsAllRedis(competitionId);
if(redisRecords != null){
return redisRecords;
}
// 此时数据库中有对应的解题记录,但是redis中没有缓存,很有可能是缓存失效 那么需要从数据库中查出所有解题记录,生成榜单
// 3.查询对应榜单
List<ProblemSolveRecord> solveRecords = getCompetitionSolveRecord(competitionId);
// 将榜单列表转化为map 键为团队id 值为解题列表
Map<String, List<ProblemSolveRecord>> solveMap = new HashMap<>();
for(ProblemSolveRecord solveRecord : solveRecords){
if(solveMap.containsKey(solveRecord.getTeamId())){
solveMap.get(solveRecord.getTeamId()).add(solveRecord);
}else{
solveMap.put(solveRecord.getTeamId(), new ArrayList<ProblemSolveRecord>());
solveMap.get(solveRecord.getTeamId()).add(solveRecord);
}
}
// 4.查询出所有团队
List<Team> teams = getAllCompetitionTeam(competitionId);
if(teams.size() == 0){
return null;
}
// 5.查询出所有的题目
List<Problem> problems = getAllCompetitionProblem(competitionId);
// 6.初始化每一条记录 数量固定,使用ArrayList
List<OneTotalRecord> records = new ArrayList<>(teams.size());
for(Team team : teams){
// 初始化基本信息
OneTotalRecord record = new OneTotalRecord(team.getTeamName(), team.getId(), team.getBelongName(), team.getCompanyOrSchool());
List<ProblemSolve> solveProblemList = record.getTitles();
// 初始化题目信息
for(Problem problem : problems){
// 初始化解题记录
solveProblemList.add(new ProblemSolve(problem.getProblemId(), problem.getProblemName(), problem.getTag()));
}
if(solveMap.containsKey(team.getId())){
List<ProblemSolveRecord> solveRecordList = solveMap.get(team.getId());
// 遍历当前团队解出的每一个题
for(int i = 0; i < solveRecordList.size(); i++){
ProblemSolveRecord solveRecord = solveRecordList.get(i);
String problemId = solveRecord.getProblemId();
// 寻找每一个题目对应的记录
for(ProblemSolve problemSolve : solveProblemList){
// 找到了对应的解题记录
if(problemSolve.getProblemId().equals(problemId)){
problemSolve.setGetScore(solveRecord.getGetScore());
problemSolve.setFirstBlood(solveRecord.getFirstBlood());
problemSolve.setSecondBlood(solveRecord.getSecondBlood());
problemSolve.setTribleBlood(solveRecord.getTribleBlood());
break;
}
}
}
solveMap.remove(team.getId());
}
}
// 7.排序
recordsSortByScore(records);
// 8.存入redis
saveRecordsToRedis(records);
return records;
}finally {
lock.unlock();
}
}
/**
* 当新增一条解题记录时的处理逻辑
* @param competitionId
* @param teamId
* @param
*/
public void addOneSolution(String competitionId, String teamId, ProblemSolve problemSolve){
lock.lock();
try{
// 1.获取之前的榜单
List<OneTotalRecord> records = getAllRecord(competitionId);
// 2.找到指定团队
for(OneTotalRecord record : records){
if(record.getId().equals(teamId)){
// 新增解题
record.solveOneProblem(problemSolve);
break;
}
}
// 3.重新排序
recordsSortByScore(records);
// 4.存入redis
saveRecordsToRedis(records);
}finally {
lock.unlock();
}
}
/**
* 后台新上了题目
* @param problem
*/
public void addOneProblem(String competitionId, Problem problem){
lock.lock();
try{
// 1.获取之前的榜单
List<OneTotalRecord> records = getAllRecord(competitionId);
// 2.给每条数据新增一个题目
for(OneTotalRecord record : records){
record.getTitles().add(new ProblemSolve(problem.getProblemId(), problem.getProblemName(), problem.getTag()));
}
// 3.无需重新排序 直接存入redis
saveRecordsToRedis(records);
}finally {
lock.unlock();
}
}
/**
* 初始化一份榜单数据
* @param competitionId
* @return
*/
private List<OneTotalRecord> initRecord(String competitionId){
lock.lock();
try{
// 某个线程获得锁时已经成功初始化
List<OneTotalRecord> redisRecords = getRecordsAllRedis(competitionId);
if(redisRecords != null){
return redisRecords;
}
// 1.先查询出所有的队伍
List<Team> teams = getAllCompetitionTeam(competitionId);
if(teams.size() == 0){
return null;
}
// 2.查询出所有的题目
List<Problem> problems = getAllCompetitionProblem(competitionId);
// 3.初始化每一条记录 数量固定,使用ArrayList
List<OneTotalRecord> records = new ArrayList<>(teams.size());
for(Team team : teams){
// 初始化基本信息
OneTotalRecord record = new OneTotalRecord(team.getTeamName(), team.getId(), team.getBelongName(), team.getCompanyOrSchool());
List<ProblemSolve> solveProblemList = record.getTitles();
// 初始化题目信息
for(Problem problem : problems){
solveProblemList.add(new ProblemSolve(problem.getProblemId(), problem.getProblemName(), problem.getTag()));
}
}
// 4.排序
recordsSortById(records);
// 5.存入redis中
saveRecordsToRedis(records);
return records;
}finally {
lock.unlock();
}
}
/**
* 对榜单按按照Id排序
* @param records
*/
private void recordsSortById(List<OneTotalRecord> records){
Collections.sort(records, new Comparator<OneTotalRecord>() {
@Override
public int compare(OneTotalRecord o1, OneTotalRecord o2) {
return o1.getId().compareTo(o2.getId());
}
});
}
/**
* 对榜单按照总分排序
* @param records
*/
private void recordsSortByScore(List<OneTotalRecord> records){
Collections.sort(records, new Comparator<OneTotalRecord>() {
@Override
public int compare(OneTotalRecord o1, OneTotalRecord o2) {
// 先按总分排
if(o2.getTotalScore().compareTo(o1.getTotalScore()) != 0){
return o2.getTotalScore().compareTo(o1.getTotalScore());
}
// 再按解出时间排序
return o1.getLastSolveTime().compareTo(o2.getLastSolveTime());
}
});
}
private List<ProblemSolveRecord> getCompetitionSolveRecord(String competitionId){
return new ArrayList<>();
}
/**
*
* @param competitionId
* @return 当前比赛是否有成功解题记录
*/
private boolean checkValidSolve(String competitionId){
// TODO 需要重写 从数据库中查询当前比赛是否有成功解题记录
return false;
}
/**
* 将榜单数据存储到redis中
* @param records
*/
private void saveRecordsToRedis(List<OneTotalRecord> records){
// TODO 需要重写 将榜单数据保存至redis中
}
/**
*
* @param competitionId
* @return 总榜单数据
*/
private List<OneTotalRecord> getRecordsAllRedis(String competitionId){
// TODO 需要重写 从redis中查询出对应比赛榜单
return null;
}
/**
*
* @param competitionId
* @return 所有参赛团队
*/
private List<Team> getAllCompetitionTeam(String competitionId){
// TODO 需要重写 从数据库中查询所有参赛团队
return new ArrayList<>();
}
/**
*
* @param competitionId
* @return 所有比赛题目
*/
private List<Problem> getAllCompetitionProblem(String competitionId){
// TODO 需要重写 从数据库中查询所有赛题
return new ArrayList<>();
}
}
6.5 实现细节
6.5.1 如何确定具体的数据结构?
存储总记录的数据结构的选择(ArrayList)
比赛一开始,就不可以再报名,也就是说,比赛过程中榜单的总记录数是确定的,不涉及动态插入删除的需求,所以可以直接使用ArryList。并且可以在构造方法中显示的传入大小。
存储单条记录的所有题目的数据结构的选择(LinkedList)
比赛题目会涉及动态的添加或删除,此时选择双链表实现的LinkedList更好。
存储数据库中解题记录的数据结构的选择(HashMap)
因为总记录是使用List存储的,所以要找到指定的队伍添加解题记录,就需要遍历整个List,如果我们将解题记录存放到Map中,键为团队ID,值为该团队解题记录的链表。这样只需要遍历一次即可把一个团队的解题记录全部添加。
6.5.2 为何要加锁
Spring默认的Service对象是单例对象,当多个线程访问的时候,访问的是同一个方法,此时,redis中存储的榜单,就相当于是共享数据,多个线程对共享数据的操作,就需要考虑线程安全的问题。
6.5.3 双检锁思想
在初始化榜单和重写从数据库查出记录生成榜单的方法中,可以看到一个细节,在获得锁后,还是需要判断一下redis中是否存在,这就是典型的双检锁思想,确保redis中的记录只生成唯一一次。
6.5.4 redis缓存策略如何设置
在代码中并没有给出redis具体缓存策略怎么实现,但是可以知道存储的都是榜单对象序列化后的结果。这样将所有对榜单的操作都放在java层中,耦合度小很多。
关于过期时间的策略,建议可以设置久一点,比如一个小时,反正如果榜单有变动,会及时刷新。
6.5.5 比赛时间到后如何处理榜单
比赛过程中,榜单全部在redis中,一旦比赛结束,榜单将不会再变动,这时候可以从数据库中查出对应的记录生成一份最终的榜单,序列化后存入数据库中,或者为每个战队生成一条记录(包含排名)存到数据库中。
7.设计方案总结
7.1 优点
- 比赛过程中,榜单几乎从redis中获取,解决了比赛过程中榜单接口调用频繁导致数据库压力过大的问题。
- 这个设计方案,返回的榜单包含了所需的所有信息,前端只需要按tag分一下类即可展示。
- 效率高,解决了线程安全问题。
7.2 缺陷
- 使用了直接加锁的方式来解决线程安全问题,一旦某个题出现集中提交且正确的情况,那么会有多个线程阻塞,线程数堆积过多,可能会导致oom。
- 在很多地方,仍然使用的直接遍历的方式去查找指定的战队,如果战队数过多,这个时间消耗也会很大。
- 对于动态分数的比赛机制,没有很好的支持。
7.3 改进思路
- 在应用层采用信号量的方式,限制提交题目接口的并发次数。
- 空间换时间,对所有战队,将其维护在一个哈希表中,这样每次找到这个战队,只需要O(1)的时间即可。
- 如果采用动态分数制,那么将所有题目和解出数全部维护在redis中,在排序前,先更新每个战队每个题目的分数。
ATFWUS 2021-11-14