Redis Sorted Set(有序集合)默认根据 Score(分值)进行升序排列。当多个成员的 Score 完全相同时,Redis 会根据 Member(成员名)的字典序进行排序。这种机制通常无法满足“按时间先到先得”或“最新到先得”的业务需求。要实现 Score 相同时按时间排序,最稳健的方法是将时间戳融入 Score 中,构建一个组合分数。
方案一:小数位拼接法(推荐)
这种方法将时间戳作为小数部分拼接在原始 Score 后面。Redis 的双精度浮点数足以支持这种操作,且直观易读。
1. 确定时间精度
选择毫秒级时间戳(13位数字)作为排序依据。如果对并发要求不高,秒级(10位)也可以。
2. 计算组合分数
将原始分数与时间戳通过数学方法组合。为了让时间戳不影响原始分数的大小比较,需将其除以一个足够大的基数(如 $10^{15}$)。
计算公式如下:
$$ FinalScore = RawScore + \frac{Timestamp}{10^{15}} $$
- $RawScore$:原始业务分数(如 100 分)。
- $Timestamp$:当前时间戳(如 1715000000000)。
- $10^{15}$:足够大的偏移量,确保时间戳部分仅在小数点后起作用。
3. 写入数据
执行 ZADD 命令将数据存入 Redis。
假设用户 A 的原始分数为 100,时间戳为 1715000000000。
计算 得分:
$$ 100 + \frac{1715000000000}{1000000000000000} = 100.001715 $$
运行 Redis 命令:
ZADD leaderboard 100.001715 "user_A"
假设用户 B 在稍后时刻(时间戳 1715000001000)也获得了 100 分。
计算 得分:
$$ 100 + \frac{1715000001000}{1000000000000000} = 100.001715000001 $$
运行 Redis 命令:
ZADD leaderboard 100.001715000001 "user_B"
由于 $100.001715 < 100.001715000001$,在默认的升序排列中,用户 A(先到的)会排在用户 B 前面。如果需要“最新的排在前面”,只需在公式中用 最大时间戳 - 当前时间戳。
方案二:整数加权法(避免浮点误差)
如果原始分数非常大,或者担心浮点数精度丢失,可以使用整数加权法。这需要将所有数据放大。
1. 设定权重因子
确定 一个足够大的乘数 $M$,使得 $M$ 大于可能出现的最大时间戳。
例如,设 $M = 10^{13}$。
2. 计算组合分数
逻辑是:新分数 = 原始分数 $\times$ 权重 + 时间戳因子。
如果希望“先到先得”(时间戳越小越靠前),应使用 (MaxTime - Timestamp) 作为因子。
计算公式如下(假设采用“先到先得”):
$$ FinalScore = RawScore \times M + (MaxTimestamp - CurrentTimestamp) $$
假设最大时间戳设定为一个未来的极大值,或者简化处理,直接用负数时间戳(如果支持负数 Score),但为了通用性,这里演示相加模式(需配合业务逻辑调整排序顺序)。
更常用的简单逻辑是:直接拼接,但这就要求分数部分不能占用所有位数。
为了绝对精确,推荐以下逻辑:
$$ FinalScore = RawScore \times 10^{13} + (2^{53} - 1 - Timestamp) $$
注:这里用大数减去时间戳是为了让时间早的(数值小的)剩下的差值大,从而排在前面(取决于排序是降序还是升序)。
最简化的“最新优先”整数计算示例(假设 Score 范围在 0-1000):
$$ FinalScore = RawScore \times 10^{13} + CurrentTimestamp $$
这样,分数不同时看高位,分数相同时看低位的时间戳。
3. 写入数据
执行 命令:
# 假设 RawScore=100, Timestamp=1715000000000
# FinalScore = 100 * 10000000000000 + 1715000000000 = 1000000017150000000000
ZADD leaderboard_int 1000000017150000000000 "user_A"
数据读取与还原
写入数据后,读取操作与普通 Sorted Set 无异,但在前端展示时,需要剥离出原始分数。
1. 读取排行榜
使用 ZRANGE 或 ZREVRANGE 获取排行榜。
# 获取分数排名前 10 的用户(包含分数)
ZRANGE leaderboard 0 9 WITHSCORES
2. 还原原始分数
程序获取到分数(如 100.001715)后,进行截断处理。
-
对于方案一(小数法):
直接取整或截取小数点前的部分。- Python:
int(score)或math.floor(score) - JavaScript:
Math.floor(score)
- Python:
-
对于方案二(整数法):
执行 整数除法。- Python/JS:
Math.floor(score / 10000000000000)
- Python/JS:
方案对比
为了更直观地选择,请参考下表:
| 特性 | 方案一:小数位拼接 | 方案二:整数加权 |
|---|---|---|
| 原理 | 将时间戳作为极小的小数加到原分上 | 将时间戳拼接到原分的低位 |
| 可读性 | 高(如 100.00171 接近原分) | 低(数值巨大,如 10000000...) |
| 精度风险 | 极低(除非原分极大,超过 $10^{15}$) | 无(纯整数运算) |
| 适用场景 | 分数较小(如万级以内),需要直观查看 | 分数范围不可控,或对浮点数敏感 |
实施流程图
以下是采用“小数位拼接法”实现“先到先得”排行榜的数据处理逻辑:
注意:如果在高并发场景下,同一毫秒内有多个用户获得相同分数,建议在时间戳后追加微秒数或随机序列号,并相应调整除数,以确保分数完全唯一,避免 Redis 字典序介入排序。

暂无评论,快来抢沙发吧!