摘要:最近复盘项目的时候,把平台中实时榜单的后端设计部分整理了一下,在此分享出来。实时榜单功能作为整个平台最重要的功能之一,同时也是访问量最高的一个功能,对这个功能做一个比较全面的设计是很有必要的。获取指定比赛的榜单。
最近复盘项目的时候,把CTF平台中实时榜单的后端设计部分整理了一下,在此分享出来。
线上CTF竞赛(夺旗赛)通常是在限定的时间内访问题目,解出flag后在平台上提交,提交后能够获得一定的分数,最后会按总分进行排序确定此次竞赛的成绩。
在CTF竞赛中,榜单功能是访问量最高的一个功能,通过查看榜单的排名,做题情况,能够感知题目的难度,方便及时调整团队做题的策略。这个榜单通常需要反映全部战队的一个做题情况,包括每个题的解出情况,大致如下图(截图来自i春秋,第二届“祥云杯”网络安全大赛暨吉林省第四届大学生网络安全大赛)。
大致会有如下功能:
实时榜单功能作为整个CTF平台最重要的功能之一,同时也是访问量最高的一个功能,对这个功能做一个比较全面的设计是很有必要的。
对实时榜单整个接口来说,一般具体以下特点:
从上面的具体情况可以发现,其实整体的排序计算开销是非常小的,因为队伍数和题目数都非常少,主要的开销集中在数据库IO,而榜单接口的访问并发非常大,如果每次访问的请求最终都打到数据库上,很有可能造成数据库宕机。
如果简单的对榜单进行缓存,那么只要有人交题,就会导致缓存失效。这个时候还是有可能导致数据库宕机。
因此我们要设计好对榜单的缓存机制,尽量少访问数据库。
通过上面的分析,我们设计榜单的关键就在如何在整个榜单维护在redis缓存中,从而降低数据库的查询次数。
在比赛期间,我们完全可以将整个榜单全部缓存到redis中,当比赛结束时,如果再次查询榜单(这个时候这个接口的访问量已经非常少了),我们可以从数据库中查出并将整个榜单存储到数据库中(因为此时整个榜单已经不会再变动)。
我们如果要保证缓存中的榜单总是实时正确的,就需要在有任何队伍分数发生变化的时候或者新增题目的时候,重新生成整份榜单并存回redis中,注意,这个时候,我们不需要从数据库中查询记录,只需要把提交记录保存到数据库,并且重新生成缓存即可。
除此之外,我们还需要注意一个问题,因为很有可能会出现多个线程对redis完成读,更新,写入的一个事务,这其中是存在线程安全问题的,针对上述提交正确次数不会过于集中的情况,我们可以对每个更新事务加锁来解决线程安全的问题。
总结一下:整体设计思路是:比赛过程中把榜单维护在redis中,比赛结束后,再将整份榜单记录写入到数据库。
以下实体类我只将与榜单功能相关的字段写出来了,实际情况可以再添加更多的字段。
存储团队信息的实体类,和数据库字段一一对应。
/** * @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 + "/"" + "}"; }}
题目的实体类,和数据库字段一一对应。
/** * @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 + "}"; }}
解题记录,和数据库字段一一对应,在使用的时候,是筛选出了所有提交正确的记录。
/** * @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; }}
不是数据库中表对应的实体类,代表对某个队伍来说,具体某个题目的解题情况,用于榜单每一行的题目显示,相关字段可以从上述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; }}
代表榜单中的一行记录,包含了一行需要显示的所有记录字段。其中对于这个团队来说的所有解题情况,都可以靠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 + "}"; }}
针对上述需求分析,我们的榜单的Service接口只需要三个public方法即可。
public List getAllRecord(String competitionId)
获取指定比赛的榜单。public void addOneSolution(String competitionId, String teamId, ProblemSolve problemSolve)
新增一条成功解题记录时,更新榜单。public void addOneProblem(String competitionId, Problem problem)
新增一个题目的时候,更新逻辑。(可以在批量上题完毕后调用一次)这里没有给出删除题目的方法,因为比赛过程中删除题目会有很多种不同的处理逻辑,需要自行去实现处理逻辑。
总体思路就是,先从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{ // 某个线程获得锁时已经成功初始化
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/123252.html
摘要:是求两个有序集合的并集,可以用来合并两个投票中所有参与的人的排行榜。经过考察技术方案和实现成本,决定采用提供的有序集合,实现投票过程和实时排名的展示,直接读取缓存,避免了非核心业务对数据库的突发高并发访问。 redis是一种提供多种数据类型的开源key-value存储系统,通常将数据全部存储在内存中。 showImg(https://segmentfault.com/img/remot...
摘要:是求两个有序集合的并集,可以用来合并两个投票中所有参与的人的排行榜。经过考察技术方案和实现成本,决定采用提供的有序集合,实现投票过程和实时排名的展示,直接读取缓存,避免了非核心业务对数据库的突发高并发访问。 redis是一种提供多种数据类型的开源key-value存储系统,通常将数据全部存储在内存中。 showImg(https://segmentfault.com/img/remot...
摘要:会上云游戏产业联盟正式公布了创新揭榜入选企业名单,凭借云游戏服务平台,在基础能力设施中的优异性能表现成功入选,并当选云游戏产业联盟副理事长单位。7月21日,由福州市人民政府、中国信息通信研究院联合主办的5G 云游戏产业博览会(2021)暨云游戏产业高峰论坛顺利开幕。会上5G云游戏产业联盟正式公布了2021创新揭榜入选企业名单,UCloud凭借云游戏服务平台,在基础能力设施中的优异性能表现成功...
摘要:为了满足产品多变的需求,我们一并实现了小时榜日榜周榜月榜几种周期榜。因此,最直观的一个方案是首先记录每天的排行榜,那么第天的最近天榜,其中,表示第天的前天的日榜。关于内存容量限制的探讨基于实现的排行榜,每个元素约需要字节内存。 本文由云+社区发表 前言 业务已基于Redis实现了一个高可用的排行榜服务,长期以来相安无事。有一天,产品说:我要一个按周排名的排行榜,以反映本周内用户的活跃情...
摘要:更多文章,欢迎点击一灰灰专题在一些游戏和活动中,当涉及到社交元素的时候,排行榜可以说是一个很常见的需求场景了,就我们通常见到的排行榜而言,会提供以下基本功能全球榜单,对所有用户根据积分进行排名,并在榜单上展示前多少个人排名,用户查询自己所 更多Spring文章,欢迎点击 一灰灰Blog-Spring专题 在一些游戏和活动中,当涉及到社交元素的时候,排行榜可以说是一个很常见的需求场景了...
阅读 3411·2021-11-15 11:37
阅读 2521·2021-08-20 09:37
阅读 1427·2019-08-30 12:47
阅读 975·2019-08-29 13:27
阅读 1593·2019-08-28 18:02
阅读 645·2019-08-23 18:15
阅读 2957·2019-08-23 16:51
阅读 818·2019-08-23 14:13