比赛进行期间,Leverage——我维护的 Online Judge 平台——的排行榜停止更新了。持续了大约半天。学生在提交代码、拿到评测结果,但排名没有变化。最终我们把问题追溯到一个每 15 分钟执行的 cron job,它严重地阻塞了 Node.js 事件循环,导致进程几乎不响应。
这篇文章讲的是哪里出了问题、为什么那些"显而易见"的修法实际上什么都没修好,以及 Redis Sorted Set 方案如何用 O(log N) 实时更新彻底替换掉整个 cron job。
原始设计
排名系统是这样工作的:
// rank.service.ts — 简化版
async rebuildSaAndRank(divisionId: number, ids: number[]) {
// 第一步:加载所有提交
const submissions = await Submission.createQueryBuilder('s')
.where('s.divisionId = :divisionId', { divisionId })
.orderBy('s.createdAt', 'ASC')
.getRawMany()
// 第二步:在内存里计算每个用户的分数
const userDatas: Map<UserId, ScoreAggregate>[] = []
for (const submission of submissions) {
// ... 处理每条提交,更新用户分数 map
// 用 cloneDeep 创建完整的每日历史快照
}
// 第三步:给所有人排序
const ranked = [...userDatas[0].entries()]
.sort(([, a], [, b]) => compareScores(a, b))
// 第四步:逐条写回——每个用户一条 UPDATE
for (const [userId, scoreAggregate] of ranked) {
await ContestUser.update({ userId, contestId }, {
rank: /* 计算出的排名 */,
score: scoreAggregate.score,
})
}
}
大约 6 分钟
