Redis 有序集合(Sorted Set / ZSet)完全攻略
“排行榜 + 延迟队列 + 优先级任务” 三合一神器
唯一带分数的集合,自动排序,O(log N) 插入,O(1) 查询排名!
一、ZSet 核心特点
| 特性 | 说明 |
|---|---|
| 底层结构 | 跳表(skiplist) + 哈希表 |
| 有序 | 按 score 升序排列 |
| 成员唯一 | 同一个 member 只能出现一次 |
| 分数可重复 | 多个 member 可有相同 score |
| 最大成员数 | 2^32 – 1(约 42 亿) |
| 核心操作 | ZADD O(log N),ZRANGE O(log N + M) |
| 内部编码 | 小集合 → ziplist,大集合 → skiplist |
二、ZSet 命令全表(共 25 个)
| 命令 | 说明 | 示例 |
|---|---|---|
ZADD key [NX|XX] [CH] [INCR] score member | 添加/更新 | ZADD rank 100 "张三" |
ZINCRBY key increment member | 加分 | ZINCRBY rank 10 "张三" |
ZSCORE key member | 获取分数 | ZSCORE rank "张三" |
ZRANGE key 0 -1 WITHSCORES | 顺序查看 | ZRANGE rank 0 9 WITHSCORES |
ZREVRANGE key 0 2 WITHSCORES | 倒序查看 | ZREVRANGE rank 0 2 WITHSCORES |
ZRANK key member | 排名(从小到大) | ZRANK rank "李四" |
ZREVRANK key member | 倒排(从大到小) | ZREVRANK rank "李四" |
ZREM key member | 删除成员 | ZREM rank "王五" |
ZREMRANGEBYRANK key 0 99 | 按排名删 | ZREMRANGEBYRANK rank 0 99 |
ZREMRANGEBYSCORE key -inf 80 | 按分数删 | ZREMRANGEBYSCORE rank -inf 80 |
ZCARD key | 成员数量 | ZCARD rank |
ZCOUNT key min max | 分数区间数量 | ZCOUNT rank 90 100 |
ZLEXCOUNT key [a [z | 字典序数量 | ZLEXCOUNT rank [a [z |
ZRANGEBYSCORE key 90 100 | 分数区间 | ZRANGEBYSCORE rank 90 100 |
ZREVRANGEBYSCORE key 100 90 | 倒序区间 | ZREVRANGEBYSCORE rank 100 90 |
ZRANGEBYLEX key [a [z | 字典序区间 | ZRANGEBYLEX rank [a [z |
ZSCAN key cursor [MATCH p] | 渐进式遍历 | ZSCAN rank 0 MATCH z* |
ZPOPMIN key [count] | 弹出最小 | ZPOPMIN rank 1 |
ZPOPMAX key [count] | 弹出最大 | ZPOPMAX rank 1 |
BZPOPMIN key timeout | 阻塞弹出最小 | BZPOPMIN delay 0 |
三、核心实战场景
1. 实时排行榜(游戏、热度)
# 用户得分
ZADD game:score 1500 "player1" 2300 "player2" 1800 "player3"
# 前10名
ZREVRANGE game:score 0 9 WITHSCORES
# 玩家排名
ZREVRANK game:score "player1" → 1(第2名)
# 加分
ZINCRBY game:score 50 "player1"
2. 延迟队列(定时任务)
# 任务在 5 秒后执行(score = 当前时间戳 + 延迟)
ZADD delay:queue 1734028860 "send_email:1001"
# 轮询执行到期任务
current = time.time()
tasks = ZRANGEBYSCORE delay:queue -inf $current
for task in tasks:
ZREM delay:queue task
# 执行任务
更推荐 Redis 7.0+
ZMPOP+ 阻塞
3. 优先级任务队列
# 高优先级任务 score 越小越优先
ZADD tasks 1 "backup_db" 3 "send_report" 10 "clean_logs"
# 取出最高优先级任务
task = ZPOPMIN tasks
4. 热点数据排序(文章热度)
# 文章浏览 = 1,点赞 = 5,评论 = 10
ZINCRBY hot:articles 1 "article:1001" # 浏览
ZINCRBY hot:articles 5 "article:1001" # 点赞
ZINCRBY hot:articles 10 "article:1001" # 评论
# 热榜前10
ZREVRANGE hot:articles 0 9 WITHSCORES
5. 时间窗口统计(滑动窗口限流)
# 记录用户请求时间戳
ZADD rate:limit:user:1001 {timestamp} "req1"
# 清理 60 秒前的数据
ZREMRANGEBYSCORE rate:limit:user:1001 -inf {now-60}
# 统计 60 秒内请求数
ZCARD rate:limit:user:1001 > 100 → 限流
四、ZSet vs Set vs List 对比
| 场景 | 推荐 |
|---|---|
| 需要排序/排名 | ZSet |
| 只需去重 | Set |
| 保持插入顺序 | List |
| 延迟/优先级队列 | ZSet |
| 高频中间插入 | List |
五、内部编码优化(ziplist vs skiplist)
# 小 ZSet → ziplist(省内存)
ZADD small 1 a 2 b 3 c
OBJECT ENCODING small → "ziplist"
# 大 ZSet → skiplist
ZADD large {1..1000 members}
OBJECT ENCODING large → "skiplist"
切换阈值
| 配置 | 默认值 | 说明 |
|---|---|---|
zset-max-ziplist-entries | 128 | 成员 > 128 → skiplist |
zset-max-ziplist-value | 64 | value > 64 字节 → skiplist |
生产建议:调大到
256提升性能
六、性能与内存优化建议
| 建议 | 说明 |
|---|---|
| 控制成员数 < 10万 | 避免 skiplist 退化 |
| 避免大 member 字符串 | >1KB 建议用 ID |
用 ZREVRANGE 替代 ZRANGE + 逆序 | 更高效 |
| 设置 TTL | EXPIRE rank:game 86400 |
用 ZSCAN 替代 ZRANGE 0 -1 | 大集合安全遍历 |
# 安全遍历
ZSCAN rank 0 MATCH "player:*" COUNT 100
七、大 ZSet 排查与拆分
排查大 ZSet
redis-cli --bigkeys
# Biggest zset found "leaderboard:global" with 1000000 members
拆分方案
# 按时间分片
ZADD rank:20251112 1500 "p1"
ZADD rank:20251113 1600 "p1"
# 按游戏/地区分片
ZADD rank:game1:cn 1500 "p1"
ZADD rank:game1:us 1400 "p2"
八、一键速查表
# 排行榜
ZADD r 100 a 200 b 150 c
ZREVRANGE r 0 2 WITHSCORES
ZREVRANK r b
ZINCRBY r 50 a
# 延迟队列
ZADD delay 1734028800 "task1"
ZRANGEBYSCORE delay -inf {now}
ZREM delay "task1"
# 管理
ZCARD r
ZCOUNT r 100 200
ZREMRANGEBYRANK r 0 99
ZPOPMIN r 1
# 遍历
ZSCAN r 0 MATCH a*
九、客户端代码示例
Python (redis-py)
import redis
import time
r = redis.Redis()
# 排行榜
r.zadd('game:score', {'player1': 1500, 'player2': 2300})
r.zincrby('game:score', 100, 'player1')
# 前3名
top3 = r.zrevrange('game:score', 0, 2, withscores=True)
print(top3) # [(b'player2', 2300.0), (b'player1', 1600.0)]
# 延迟任务
r.zadd('delay:queue', {'send_sms': time.time() + 5})
# 轮询执行...
Go (go-redis)
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
rdb.ZAdd(ctx, "rank", redis.Z{Score: 100, Member: "p1"})
score, _ := rdb.ZScore(ctx, "rank", "p1").Result()
十、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 内存暴涨 | ZSet 无限增长 | 加 TTL + 分片 |
ZRANGE 0 -1 卡顿 | 成员 > 100万 | 改用 ZSCAN |
| 排名不准 | 并发加分 | ZINCRBY 原子操作 |
| 分数溢出 | 使用整数 | 用 double |
十一、ZSet 在高并发中的终极用法
1. 原子扣库存 + 排行榜
EVAL "
local stock = redis.call('zscore', KEYS[1], KEYS[2])
if stock and stock >= 1 then
redis.call('zincrby', KEYS[1], -1, KEYS[2])
return 1
end
return 0
" 2 stock:goods:1001 user:order:001
2. 阻塞延迟队列(Redis 7.0+)
# 消费者阻塞等待到期任务
task = BZPOPMIN delay:queue 0
完成!你已精通 Redis ZSet!
# 一行命令体验 ZSet 全功能
redis-cli <<EOF
ZADD r 1 a 2 b 3 c
ZINCRBY r 5 a
ZREVRANGE r 0 -1 WITHSCORES
ZREVRANK r b
ZPOPMIN r
ZCARD r
EOF
下一步推荐:
需要我送你:
- “全球百万玩家实时排行榜方案”?
- “精准延迟队列(ZSet + BZPOPMIN)”?
- “热点文章排名(多维度加权)”?
回复:全球榜 | 延迟队列 | 热点榜 即可!