资讯专栏INFORMATION COLUMN

CTF平台实时榜单功能后端设计方案

Luosunce / 3410人阅读

摘要:最近复盘项目的时候,把平台中实时榜单的后端设计部分整理了一下,在此分享出来。实时榜单功能作为整个平台最重要的功能之一,同时也是访问量最高的一个功能,对这个功能做一个比较全面的设计是很有必要的。获取指定比赛的榜单。

最近复盘项目的时候,把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 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{            // 某个线程获得锁时已经成功初始化                
               
                                           
                       
                 

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/123252.html

相关文章

  • 玩转Redis - 使用有序集合(sorted sets)实现投票游戏

    摘要:是求两个有序集合的并集,可以用来合并两个投票中所有参与的人的排行榜。经过考察技术方案和实现成本,决定采用提供的有序集合,实现投票过程和实时排名的展示,直接读取缓存,避免了非核心业务对数据库的突发高并发访问。 redis是一种提供多种数据类型的开源key-value存储系统,通常将数据全部存储在内存中。 showImg(https://segmentfault.com/img/remot...

    AndroidTraveler 评论0 收藏0
  • 玩转Redis - 使用有序集合(sorted sets)实现投票游戏

    摘要:是求两个有序集合的并集,可以用来合并两个投票中所有参与的人的排行榜。经过考察技术方案和实现成本,决定采用提供的有序集合,实现投票过程和实时排名的展示,直接读取缓存,避免了非核心业务对数据库的突发高并发访问。 redis是一种提供多种数据类型的开源key-value存储系统,通常将数据全部存储在内存中。 showImg(https://segmentfault.com/img/remot...

    LeoHsiun 评论0 收藏0
  • UCloud入选2021云游戏服务企业“创新单”

    摘要:会上云游戏产业联盟正式公布了创新揭榜入选企业名单,凭借云游戏服务平台,在基础能力设施中的优异性能表现成功入选,并当选云游戏产业联盟副理事长单位。7月21日,由福州市人民政府、中国信息通信研究院联合主办的5G 云游戏产业博览会(2021)暨云游戏产业高峰论坛顺利开幕。会上5G云游戏产业联盟正式公布了2021创新揭榜入选企业名单,UCloud凭借云游戏服务平台,在基础能力设施中的优异性能表现成功...

    Tecode 评论0 收藏0
  • 想知道谁是你的最佳用户?基于Redis实现排行周期与最近N期

    摘要:为了满足产品多变的需求,我们一并实现了小时榜日榜周榜月榜几种周期榜。因此,最直观的一个方案是首先记录每天的排行榜,那么第天的最近天榜,其中,表示第天的前天的日榜。关于内存容量限制的探讨基于实现的排行榜,每个元素约需要字节内存。 本文由云+社区发表 前言 业务已基于Redis实现了一个高可用的排行榜服务,长期以来相安无事。有一天,产品说:我要一个按周排名的排行榜,以反映本周内用户的活跃情...

    figofuture 评论0 收藏0
  • SpringBoot系列教程-应用篇之借助Redis实现排行功能

    摘要:更多文章,欢迎点击一灰灰专题在一些游戏和活动中,当涉及到社交元素的时候,排行榜可以说是一个很常见的需求场景了,就我们通常见到的排行榜而言,会提供以下基本功能全球榜单,对所有用户根据积分进行排名,并在榜单上展示前多少个人排名,用户查询自己所 更多Spring文章,欢迎点击 一灰灰Blog-Spring专题 在一些游戏和活动中,当涉及到社交元素的时候,排行榜可以说是一个很常见的需求场景了...

    chavesgu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<