zset 的 score 只能是浮点数,需将多维权重压缩为单个数值:按量级错位相加(如 quality×10⁶ + (ts%10⁶)×100 + hot),用 lua 脚本统一计算防精度差异,避免 zincrby 局部更新,并控制总分在 1e14 内防精度丢失。

ZSet 的 score 只能存一个数字,怎么塞进多个权重?
直接往 ZSet 的 score 里硬塞多个维度(比如热度+时间+质量)不行——Redis 不支持复合 score,它只认浮点数。常见错误是试图用 JSON 字符串当 score:ZADD myzset "{'hot':10,'time':1698765432}" item1,结果 Redis 报错 ERR value is not a valid float。
真正可行的做法是把多维数据「压缩」成单个浮点数。核心思路:给每个维度分配权重区间,错位相加,避免干扰。比如:
- 热度(0–1000)占低 6 位:
hot * 1 - 时间戳(秒级,如 1717023456)取后 6 位(避免过大)再左移 6 位:
(ts % 1000000) * 100 - 质量分(0–10)左移 12 位:
quality * 1000000 - 最终 score =
quality * 1000000 + (ts % 1000000) * 100 + hot
这样 quality 改动 1,就比时间戳变动整整 100 秒还影响大;时间戳变化 1 秒,也远超热度变化 100 点。顺序和量级必须手动对齐,否则排序会乱。
用 Lua 脚本动态算分,避免客户端反复解析
如果每次插入都靠应用层算 score,容易在不同语言/版本间出现浮点精度不一致(比如 Python 的 round() 和 Java 的 Math.round() 对负数处理不同),导致同一组输入算出不同 score,ZSet 排序错乱。
更稳的方式是把计算逻辑下沉到 Redis 服务端,用 EVAL 执行 Lua:
EVAL "return ARGV[1]*1000000 + (ARGV[2]%1000000)*100 + ARGV[3]" 0 3 1717023456 95
注意三点:
-
ARGV是字符串数组,Lua 里要显式转成 number:tonumber(ARGV[1]) - 不要在 Lua 里做耗时操作(如遍历大集合),否则阻塞 Redis 主线程
- 脚本里不能调用
redis.call('TIME')这类命令来实时取时间——ZSet 需要确定性 score,时间必须由客户端传入
分数溢出和精度丢失的真实风险
Redis 的 score 内部用 double 存储,有效精度约 15 位十进制数字。一旦你把三个维度全拉到高位(比如质量 × 1e12 + 时间 × 1e6 + 热度),总和超过 9007199254740992(2^53),就会开始丢精度——两个本该不同的 score 可能被存成同一个值,排序失效。
典型踩坑场景:
- 用完整时间戳(10 位)直接 × 1e6 → 瞬间突破 16 位,
1717023456000000已经无法精确表示 - 把用户 ID(长整型)当维度混入 score,ID 超过 1e12 就大概率出问题
- 用
Math.random()补低位防重复,但 JS 的Math.random()只有 12 位有效小数,乘上大基数后完全无效
对策很简单:所有参与计算的原始值先做归一化(比如时间戳 mod 1000000),再控制最终 score 在 1e14 以内。
更新权重时,ZSet 的 ZINCRBY 不能直接用
看到“要提升某条数据的热度”,第一反应可能是 ZINCRBY myzset 10 item_key。但这是错的——ZINCRBY 是对现有 score 做加法,而你的 score 是多维合成值,单独加 10 会破坏各维度间的相对权重关系,相当于把“质量”和“时间”一起拖低了。
正确做法永远是重新计算整个 score 后执行 ZADD(同 key 会自动覆盖):
- 先
ZSCORE myzset item_key拿旧值(可选,用于对比) - 在客户端或 Lua 中,用最新热度、当前时间、固定质量分,重算 score
- 再
ZADD myzset <new_score> item_key</new_score>
别省这一步。ZSet 没有“局部更新 score 某一位”的原子操作,强行绕开只会让排序逻辑越来越难维护。
多维排序不是拼积木,是做标尺校准。每个维度的量纲、范围、更新频率,都得在算分前想清楚——少一个约束,后面查半天为啥 itemA 总排在 itemB 前面,其实只是时间戳 mod 错了一位。










