文章目录

Redis Sorted Set实现排行榜的Score相同时按时间排序

发布于 2026-05-03 00:26:59 · 浏览 3 次 · 评论 0 条

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. 读取排行榜

使用 ZRANGEZREVRANGE 获取排行榜。

# 获取分数排名前 10 的用户(包含分数)
ZRANGE leaderboard 0 9 WITHSCORES

2. 还原原始分数

程序获取到分数(如 100.001715)后,进行截断处理。

  • 对于方案一(小数法)
    直接取整截取小数点前的部分。

    • Python: int(score)math.floor(score)
    • JavaScript: Math.floor(score)
  • 对于方案二(整数法)
    执行 整数除法。

    • Python/JS: Math.floor(score / 10000000000000)

方案对比

为了更直观地选择,请参考下表:

特性 方案一:小数位拼接 方案二:整数加权
原理 将时间戳作为极小的小数加到原分上 将时间戳拼接到原分的低位
可读性 高(如 100.00171 接近原分) 低(数值巨大,如 10000000...)
精度风险 极低(除非原分极大,超过 $10^{15}$) 无(纯整数运算)
适用场景 分数较小(如万级以内),需要直观查看 分数范围不可控,或对浮点数敏感

实施流程图

以下是采用“小数位拼接法”实现“先到先得”排行榜的数据处理逻辑:

graph LR A["用户获得分数: RawScore"] --> B["获取当前时间戳: Timestamp"] B --> C["计算组合分数: RawScore + Timestamp / 10^15"] C --> D["执行 ZADD 写入 Redis"] D --> E["执行 ZRANGE 读取排行榜"] E --> F["前端取整显示: floor FinalScore"]

注意:如果在高并发场景下,同一毫秒内有多个用户获得相同分数,建议在时间戳后追加微秒数或随机序列号,并相应调整除数,以确保分数完全唯一,避免 Redis 字典序介入排序。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文