今年上半年,我接了NOJ的第一个真正意义上的锅——重构比赛榜单。

期末考试结束,我终于可以好好谈谈这个重构是怎么实现的了。

之前的比赛榜单是这样生成的:

先去执行一个类似SELECT * FROM submission WHERE cid = $cid的SQL查询,然后根据查询到的赛中提交记录实时算一个榜单出来。这样做有什么问题呢?显而易见,那就是太慢了。每一个查询榜单的请求都需要调用一遍contestRank函数,contestRank函数再去调用一遍contestRankCache函数。但问题就是这个contestRankCache其实并不是cache。它这个函数的作用其实是传入一个包含所有比赛提交的集合对象,然后返回一个榜单的二维数组。由于计算的过程过于复杂,所以当计算榜单的时候会侵占学校垃圾的比赛服务器高达50%的CPU性能,更不用说多人同时查看榜单的情况了。记得一次榜单对外开放的比赛中途突然传出xxxAK的消息,大家冲进去看榜单,直接把服务器挤炸了,害的比赛选手交题时Submission Error。

所以,重构这个臭名昭著的榜单的重任就落在了我的头上。重构的思路是这样的:基于Redis驱动的Cache,以cid(比赛id)为键,比赛开始时建立一个空二维数组,当有选手交题时,根据单条交题记录去更新这个数组,该数组在Cache中永久保存。

这样一来,更新榜单的事件驱动就由请求榜单转为选手提交,大大减轻了服务器压力,请求榜单时也可以做到理论上的“秒开”。

以下是核心代码:

public function updateContestRankTable($cid,$sub){
    $lock = Cache::lock("contestrank$cid",10);
    try{
        if($lock->get()){
            if(Cache::tags(['contest','rank'])->get($cid) != null){
                $chache = Cache::tags(['contest','data'])->get($cid);
                $ret = Cache::tags(['contest','rank'])->get($cid);
                if (time() > $chache['frozen_time'])
                    return;

                $id = 0;

                foreach($chache['problemSet'] as $key => $p){
                    if ($p['pid'] == $sub['pid']){
                        $chache['problemSet'][$key]['cpid'] = $key;
                        $id = $key;
                    }
                }

                $ret = $this->updateContestRankDetail($chache['contest_info'],$chache['problemSet'][$id],$cid,$sub['uid'],$ret);
                $ret = $this->sortContestRankTable($chache['contest_info'],$cid,$ret);

                Cache::tags(['contest', 'rank'])->put($cid, $ret);
            }
            else{
                $ret=[];
                $chache=[];
                $chache['contest_info']=DB::table("contest")->where("cid", $cid)->first();
                $chache['problemSet']=DB::table("contest_problem")->join("problem", "contest_problem.pid", "=", "problem.pid")->where([
                    "cid"=>$cid
                ])->orderBy('ncode', 'asc')->select("ncode", "alias", "contest_problem.pid as pid", "title")->get()->all();
                $chache['frozen_time']=DB::table("contest")->where(["cid"=>$cid])->select(DB::raw("UNIX_TIMESTAMP(end_time)-froze_length as frozen_time"))->first()["frozen_time"];
                $chache['end_time']=strtotime(DB::table("contest")->where(["cid"=>$cid])->select("end_time")->first()["end_time"]);

                Cache::tags(['contest', 'data'])->put($cid, $chache);

                if ($chache['contest_info']["registration"]) {
                    $submissionUsers=DB::table("contest_participant")->where([
                        "cid"=>$cid,
                        "audit"=>1
                    ])->select('uid')->get()->all();
                }else{
                    $submissionUsers=DB::table("submission")->where([
                        "cid"=>$cid
                    ])->where(
                        "submission_date",
                        "<",
                        $chache['frozen_time']
                    )->select('uid')->groupBy('uid')->get()->all();
                }
                foreach ($submissionUsers as $s) {
                    foreach ($chache['problemSet'] as $key => $p) {
                        $p['cpid'] = $key;
                        $ret = $this->updateContestRankDetail($chache['contest_info'],$p,$cid,$s['uid'],$ret);
                    }
                }
                $ret = $this->sortContestRankTable($chache['contest_info'],$cid,$ret);
                Cache::tags(['contest', 'rank'])->put($cid, $ret);
            }
        }
    }catch(LockTimeoutException $e){
        Log::warning("Contest Rank Lock Timed Out");
    }finally{
        optional($lock)->release();
    }
}

public function sortContestRankTable($contest_info,$cid,$ret){
    if ($contest_info["rule"]==1){
        usort($ret, function ($a, $b) {
            if ($a["score"]==$b["score"]) {
                if ($a["penalty"]==$b["penalty"]) {
                    return 0;
                } elseif (($a["penalty"]>$b["penalty"])) {
                    return 1;
                } else {
                    return -1;
                }
            } elseif ($a["score"]>$b["score"]) {
                return -1;
            } else {
                return 1;
            }
        });
    }else if ($contest_info["rule"]==2){
        usort($ret, function ($a, $b) {
            if ($a["score"]==$b["score"]) {
                if ($a["solved"]==$b["solved"]) {
                    return 0;
                } elseif (($a["solved"]<$b["solved"])) {
                    return 1;
                } else {
                    return -1;
                }
            } elseif ($a["score"]>$b["score"]) {
                return -1;
            } else {
                return 1;
            }
        });
    }
    return $ret;
}

public function updateContestRankDetail($contest_info,$problem,$cid,$uid,$ret){
    $id = count($ret);
    foreach($ret as $key => $r){
        if($r['uid'] == $uid)
            $id = $key;
    }
    if ($contest_info["rule"]==1) {
        // ACM/ICPC Mode
            if($id == count($ret)){
                $prob_detail = [];
                $totPen = 0;
                $totScore = 0;
            }else{
                $prob_detail = $ret[$id]['problem_detail'];
                $totPen=$ret[$id]['penalty'];
                $totScore=$ret[$id]['score'];
            };

            $prob_stat=$this->contestProblemInfoACM($cid, $problem["pid"], $uid);

            $prob_detail[$problem['cpid']]=[
                "ncode"=>$problem["ncode"],
                "pid"=>$problem["pid"],
                "color"=>$prob_stat["color"],
                "wrong_doings"=>$prob_stat["wrong_doings"],
                "solved_time_parsed"=>$prob_stat["solved_time_parsed"]
            ];
            if ($prob_stat["solved"]) {
                $totPen+=$prob_stat["wrong_doings"] * 20;
                $totPen+=$prob_stat["solved_time"] / 60;
                $totScore+=$prob_stat["solved"];
            }

            $ret[$id]=[
                "uid" => $uid,
                "name" => DB::table("users")->where([
                    "id"=>$uid
                ])->first()["name"],
                "nick_name" => DB::table("group_member")->where([
                    "uid" => $uid,
                    "gid" => $contest_info["gid"]
                ])->where("role", ">", 0)->first()["nick_name"],
                "score" => $totScore,
                "penalty" => $totPen,
                "problem_detail" => $prob_detail
            ];
    } elseif ($contest_info["rule"]==2) {
        // OI Mode
        if($id == count($ret)){
            $prob_detail = [];
            $totPen = 0;
            $totScore = 0;
        }else{
            $prob_detail = $ret[$id]['problem_detail'];
            $totPen=$ret[$id]['penalty'];
            $totScore=$ret[$id]['score'];
        };

        $prob_stat=$this->contestProblemInfoOI($cid, $problem["pid"], $uid);
        $prob_detail[$problem['cpid']]=[
            "ncode"=>$problem["ncode"],
            "pid"=>$problem["pid"],
            "color"=>$prob_stat["color"],
            "score"=>$prob_stat["score"],
            "score_parsed"=>$prob_stat["score_parsed"]
        ];
        $totSolved+=$prob_stat["solved"];
        $totScore+=intval($prob_stat["score_parsed"]);

        $ret[$id]=[
            "uid" => $uid,
            "name" => DB::table("users")->where([
                "id"=> $uid
            ])->first()["name"],
            "nick_name" => DB::table("group_member")->where([
                "uid" => $uid,
                "gid" => $contest_info["gid"]
            ])->where("role", ">", 0)->first()["nick_name"],
            "score" => $totScore,
            "solved" => $totSolved,
            "problem_detail" => $prob_detail
        ];
    }
    return $ret;
}

唉,不提了,写Contest Admin Portal去了。

Privacy Preference Center