redis zset 在 c# 中必须用 stackexchange.redis 的 sortedset 方法(如 sortedsetadd、sortedsetrangebyrank)直接调用,score 必须为 double 类型,member 建议用 string 或 byte[],避免自动序列化;排行榜用 sortedsetrangebyrankwithscores + order.descending,延时队列需 lua 脚本原子执行 zrangebyscore + zremrangebyscore。

Redis ZSet 在 C# 中必须用 StackExchange.Redis 的 SortedSet 方法
直接调用 Database.SortedSetAdd 或 Database.SortedSetRangeByRank 才算真正操作 ZSet,别被“ZSet”字面迷惑去查 HashSet 或自己封装排序逻辑。StackExchange.Redis 是目前 .NET 生态中唯一稳定支持 Redis 原生命令的客户端,其他如 ServiceStack.Redis 已停止维护,且不兼容 Redis 7+ 的部分 ZSet 新行为(比如 LT/GT 选项)。
常见错误是把 SortedSetAdd 的 score 参数传成字符串或 null —— 它必须是 double 类型,哪怕你业务上只用整数排名,也得写成 100.0,否则会静默失败或插入默认值 0。
-
score为NaN、PositiveInfinity或NegativeInfinity会导致命令直接报错ERR invalid float - 成员(member)是
RedisValue,建议统一用string或byte[],避免用对象自动序列化——ZSet 本身不存结构,序列化后无法做范围查询或原子更新 - 批量添加用
SortedSetAddAsync(key, values),其中values是SortedSetEntry[],比循环调用快 3–5 倍(实测万级数据)
游戏排行榜:用 ZSetRangeByRank + WithScores 实现分页 TopN
别用 ZRange 或 ZRevRange 这类旧别名,StackExchange.Redis 对应的是 SortedSetRangeByRank(升序)和 SortedSetRangeByRankWithScores(带分数)。游戏排行榜通常要「从高到低」展示,所以得传 Order.Descending,且起始位置从 0 开始——但注意:Redis 的 rank 是从 0 起算,而玩家看到的“第 1 名”对应 rank 0,不是 1。
示例:取前 100 名,含分数,跳过前 200 名(即第 201–300 名):
var entries = await db.SortedSetRangeByRankWithScoresAsync(
"leaderboard:level",
start: 200,
stop: 299,
order: Order.Descending);
- 如果 stop 设为 -1,表示“到末尾”,但别写
stop: -1后还加order: Order.Descending—— 这会导致语义冲突,结果不可预测 - 想查某个玩家的实时排名,用
SortedSetRankAsync(key, member, Order.Descending),返回long?;若为null表示该 member 不存在 - 分数相同时,Redis 按 member 字典序排,无法自定义 tie-breaker;如需“同分时按登录时间先后”,得在 member 里拼接时间戳,例如
"player_123:1712345678"
延时队列:ZSet + Lua 脚本实现精确触发,避开轮询陷阱
纯靠 SortedSetRangeByScore 扫描 + 删除,再丢进普通队列(如 ConcurrentQueue),在高并发下会漏任务或重复执行。正确做法是用 Lua 脚本原子地:① 取出所有 score ≤ 当前时间戳的元素;② 从 ZSet 中移除它们;③ 返回这批元素。这样能确保“取出即失效”。
Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。
脚本示例(保存为 fetch_delayed.lua):
local items = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
if #items > 0 then
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
end
return items
在 C# 中执行:
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var members = await db.ScriptEvaluateAsync(
luaScript,
new RedisKey[] { "delayed:queue" },
new RedisValue[] { now });
- 别用
DateTime.Now.ToUnixTimeSeconds()—— 服务器时区或 NTP 偏移会导致任务提前或延迟触发 - ZSet 的 score 建议用秒级时间戳(
long),别用毫秒——Redis 内部精度是毫秒,但double表达大毫秒值会丢失精度,导致ZRANGEBYSCORE漏匹配 - 脚本返回的是
RedisResult,需转成string[]或byte[][],不能直接当string用
ZSet 性能与边界:100 万数据下 Rank 查询变慢怎么办
Redis ZSet 的 ZRank 时间复杂度是 O(log N),理论上百万级也很快,但实际中如果你频繁查“某个玩家当前排名”,而这个玩家分数常年卡在中间段,就会触发大量内存跳表遍历。更糟的是,如果用 SortedSetLength 获取总人数再算“倒数第 X 名”,等于每次都要全量统计——这在 ZCard 虽是 O(1),但前提是 key 存在;若 key 为空,SortedSetLength 返回 0,容易掩盖数据未写入的问题。
- 高频查单个 rank 的场景,建议加一层本地缓存(如
MemoryCache),设置 5 秒过期,避免打穿 Redis - 不要用
ZCount统计“分数在某区间的人数”来替代分页——它不返回成员,且区间过大时仍会扫描较多节点 - ZSet 最大成员数无硬限制,但单 key 超过 500 万成员时,BGSAVE 和 AOF rewrite 会明显拖慢主线程;游戏排行榜建议按分区建 key,例如
leaderboard:level:2024Q2
ZSet 看似简单,但 score 的类型精度、member 的序列化方式、Lua 脚本的原子边界、以及高并发下 rank 查询的缓存策略——这些地方一松动,线上就出毛刺。别迷信“封装个工具类就完事”,得盯着每一步 Redis 命令的实际行为。










