TT Lab
开始
学习 学习路径 课程

Redis 与缓存

用 ZSet 实现排行榜并验证

在 TT Lab 中继续学习

目标

仅用一个有序集合,实现实时排行榜的全部需求——前 N 名、个人名次、分页、同分规则、按时间段的排名、合并汇总——并用数字确认查询性能。

为什么重要

排名是关系型 DB 最不擅长的事情之一。前 10 名靠索引很容易,但“我的名次”需要把分数比我高的人全部数一遍,所以排名靠后的某个用户的一次查询,就会扫过 99 万行。而且这条查询每个用户每次请求都会发起,与此同时,分数更新的写入也挤在同一个索引上。有序集合同时维护跳表和哈希表,使名次查询和范围查询都是 O(log N)。无论是 100 万用户还是 1000 万用户,都是毫秒级。这个实验特别要留意的是第 6 步的同分处理。Redis 默认的同分规则,是按成员字符串的字典序排列,而产品需求几乎总是“先达成的人排在前面”。把分数和时间合成一个浮点数的这种技巧,每次做排行榜都会用到。

步骤

  1. 用 /root/lb/load.py 读取 /opt/fixtures/rd/plays.csv(表头为 user_id,points,ts),按用户把累计分数放入 lb:global。ZCARD lb:global 必须是 200。
  2. 把前 10 名连同分数,以 <순위> <user_id> <점수> 的格式(占位符依次为名次、user_id 与分数)写入 /root/lb/top10.txt,共 10 行。第 1 名是第一行。
  3. 在 /root/lb/myrank.txt 中,把用户 u042 的信息写成一行 user=u042 rank=<1부터> score=<점수>(占位符依次为从 1 开始的名次与分数)。
  4. 用 /root/lb/incr.py 以原子方式给 u042 加 50 分。源码中不能出现用 ZSCORE 读出再用 ZADD 写入的模式。运行之后分数必须正好增加 50。
  5. 在 /root/lb/page.txt 中,以 <순위> <user_id> 的格式(占位符依次为名次与 user_id)写入第 100 名到第 109 名的 10 个人。第一行的名次必须是 100。
  6. 用 /root/lb/tie.py 创建 lb:tie。分数按 points + (1 - ts / 1e10) 合成。让 /opt/fixtures/rd/ties.csv 中的 5 个同分者按 ts 升序排在前面,并把结果写入 /root/lb/tie.txt,共 5 行。
  7. 创建 lb:weekly:2026-W34,并把 TTL 设为不超过 604800。用 ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2 创建合并汇总的键。ZCARD lb:combined 必须大于等于 200。
  8. 在 127.0.0.1:8150 上启动 /root/lb/api.py。GET /rank/u042 返回 {"user":"u042","rank":<정수>,"score":<수>,"top3":[...]}(占位符依次为整数与数值)。在 /root/lb/bench.txt 中写入 queries=1000 total_ms=<정수> avg_ms=<수>(占位符依次为整数、整数与数值),其中 avg_ms 必须小于 5。

参考

用游玩记录填充排行榜

用 /root/lb/load.py 读取 /opt/fixtures/rd/plays.csv(表头为 user_id,points,ts),按用户把累计分数放入 lb:global。ZCARD lb:global 必须是 200。

读取 /opt/fixtures/rd/plays.csv,把每个用户的分数放入有序集合。同一个用户会出现多次——必须累加。

取出前 10 名

把前 10 名连同分数,以 <순위> <user_id> <점수> 的格式(占位符依次为名次、user_id 与分数)写入 /root/lb/top10.txt,共 10 行。第 1 名是第一行。

有一个范围命令可以从高分开始取。还要同时取到分数,才能写进文件。

求某个用户的名次

在 /root/lb/myrank.txt 中,把用户 u042 的信息写成一行 user=u042 rank=<1부터> score=<점수>(占位符依次为从 1 开始的名次与分数)。

名次命令是从 0 开始计数的。展示给人看的时候要加 1。

以原子方式累加分数

用 /root/lb/incr.py 以原子方式给 u042 加 50 分。源码中不能出现用 ZSCORE 读出再用 ZADD 写入的模式。运行之后分数必须正好增加 50。

如果读出来、相加、再写回去,并发请求时数值会丢失。有一条命令可以直接完成相加。

取出 100 名前后的一页

在 /root/lb/page.txt 中,以 <순위> <user_id> 的格式(占位符依次为名次与 user_id)写入第 100 名到第 109 名的 10 个人。第一行的名次必须是 100。

要计算范围命令的起止索引。注意第 100 名对应的索引是多少。

把同分者按先达成的顺序排序

用 /root/lb/tie.py 创建 lb:tie。分数按 points + (1 - ts / 1e10) 合成。让 /opt/fixtures/rd/ties.csv 中的 5 个同分者按 ts 升序排在前面,并把结果写入 /root/lb/tie.txt,共 5 行。

把分数和时间合成一个浮点数。小数部分必须在 0 到 1 之间,才不会越过整数的边界。

创建周排名与合并汇总

创建 lb:weekly:2026-W34,并把 TTL 设为不超过 604800。用 ZUNIONSTORE lb:combined 2 lb:global lb:weekly:2026-W34 WEIGHTS 1 2 创建合并汇总的键。ZCARD lb:combined 必须大于等于 200。

把时间段放进键名并设置过期,清理就是自动的。有专门的命令可以带着权重把两个集合合并。

启动名次查询 API 并测量性能

在 127.0.0.1:8150 上启动 /root/lb/api.py。GET /rank/u042 返回 {"user":"u042","rank":<정수>,"score":<수>,"top3":[...]}(占位符依次为整数与数值)。在 /root/lb/bench.txt 中写入 queries=1000 total_ms=<정수> avg_ms=<수>(占位符依次为整数、整数与数值),其中 avg_ms 必须小于 5。

把前面做好的东西合到一个端点里。测一下 1,000 次查询的耗时,就能用数字看出为什么要用它而不是 DB。