有序集合(zset)是 Redis 里最有用的结构之一:每个元素带一个分数,按分数自动排序。排行榜和延时队列都是它的直接应用。
排行榜
# 加分
ZINCRBY rank:2025-07 1 user:1001
# 前 10 名(降序,带分数)
ZREVRANGE rank:2025-07 0 9 WITHSCORES
# 某个用户的排名(从 0 开始,降序)
ZREVRANK rank:2025-07 user:1001
# 分数区间(比如 100~200 分之间)
ZRANGEBYSCORE rank:2025-07 100 200
ZINCRBY 是原子操作,并发安全,不用先读再写。
排名分页
ZREVRANGE start stop 就是分页,start/stop 是索引:
# 第 3 页,每页 20 条
ZREVRANGE rank:2025-07 40 59 WITHSCORES
注意深分页依然是 O(log N + M),M 是返回条数,比数据库的 LIMIT 100000, 20 快得多。但返回的数据量大了还是慢,前 1000 名够用了。
并列怎么处理
分数相同时,Redis 按成员的字典序排列。如果想「先到先得」,把时间戳编进分数:
score = points * 10**10 + (10**10 - int(time.time()))
redis.zincrby(key, 1, user_id) # 这个不适合,因为分数会重算
更实际的做法是分数存 points,需要展示并列时在应用层二次排序。硬编码进分数会让加减分变复杂。
延时队列
思路:member 是任务 id,score 是执行时间戳(毫秒)。消费者轮询取出到期的:
import time
def push(task_id, delay_seconds):
redis.zadd("delay:queue", {task_id: time.time() + delay_seconds})
def pop_due(limit=10):
now = time.time()
ids = redis.zrangebyscore("delay:queue", 0, now, start=0, num=limit)
for tid in ids:
# 用 ZREM 的返回值做抢占,多消费者安全
if redis.zrem("delay:queue", tid):
yield tid
def worker():
while True:
for tid in pop_due():
handle(tid)
time.sleep(1)
关键点:用 ZREM 的返回值判断是否抢到。ZRANGEBYSCORE 只是读,多个消费者会读到同一条;ZREM 删成功(返回 1)的那个才继续处理,这是原子的。
可靠性
上面的实现有个问题:取出来之后进程崩了,任务就丢了。要可靠的话,先转移到「处理中」队列,处理完再删:
ZADD processing:{ts} task # 转移
# 处理成功后
ZREM processing:{ts} task
# 定时扫描 processing,超时未完成的重新入队
或者直接用 Redis 5.0 的 Stream + 消费组,支持 ack 和 pending 列表,比 zset 方案完整。
什么时候不该用
- 任务量大(百万级)、需要持久化和重放 → 用 RabbitMQ / Kafka
- 需要精确到毫秒的定时 → Redis 轮询精度不够,用专门的时间轮
- 单机就够 → 数据库表 + 定时任务更简单
一句话
zset 的分数可以是「积分」也可以是「时间戳」,同一个结构解决两个看似无关的问题。理解了这一点,很多场景都能套上来。
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://mi-blog.cn/archives/697
共有 0 条评论