Skip to content

在Redis中使用ZSet实现排行榜,当分数相同时按时间顺序排序,可以通过以下步骤实现:

实现步骤

  1. 数据结构设计

    • 主结构:使用ZSet存储用户得分,键为leaderboard,成员为用户ID,分数为复合分数
    • 辅助结构:使用Hash存储每个用户的原始分数和最新时间戳,键为user:{userId},字段为scoretimestamp
  2. 复合分数计算

    • 公式复合分数 = 原始分数 + (T_max - timestamp) / D
      • T_max:未来足够大的时间戳(如3000年的毫秒级时间戳,设为3e13)。
      • D:大于T_max的常数(如D = T_max + 1),确保(T_max - timestamp)/D < 1
    • 效果:原始分数决定整数部分,时间戳决定小数部分,时间越早(timestamp小)的成员复合分数越高。
  3. 更新用户分数

    • 获取当前时间戳current_ts
    • 计算用户新的复合分数。
    • 更新ZSet和Hash:
      bash
      # 更新ZSet中的复合分数
      ZADD leaderboard {new_score} {user_id}
      # 存储原始分数和时间戳到Hash
      HSET user:{user_id} score {original_score} timestamp {current_ts}
  4. 查询排行榜

    • 使用ZREVRANGE按分数从高到低获取排名:
      bash
      ZREVRANGE leaderboard 0 10 WITHSCORES

关键代码示例

python
import redis
import time

# 初始化Redis连接
r = redis.Redis()

# 预定义参数
T_MAX = 3e13  # 未来时间戳(如3000年)
D = T_MAX + 1  # 分母

def update_user_score(user_id, score_increment):
    # 获取当前时间戳和原始分数
    current_ts = int(time.time() * 1000)
    original_score = int(r.hget(f"user:{user_id}", "score") or 0)
    new_score = original_score + score_increment
    
    # 计算复合分数
    composite_score = new_score + (T_MAX - current_ts) / D
    
    # 更新ZSet和Hash
    r.zadd("leaderboard", {user_id: composite_score})
    r.hset(f"user:{user_id}", mapping={"score": new_score, "timestamp": current_ts})

def get_leaderboard(top_n):
    # 获取排行榜前N名
    return r.zrevrange("leaderboard", 0, top_n-1, withscores=True)

# 示例:用户A得分增加10,当前时间戳为1620000000000
update_user_score("userA", 10)
print(get_leaderboard(10))

技术细节

  • 时间戳处理:使用毫秒级时间戳确保精度,避免时钟回拨问题(可通过NTP同步解决)。
  • 浮点数精度:双精度浮点数可精确表示15-17位小数,足够处理T_max - timestamp的差值。
  • 数据一致性:通过事务或Lua脚本保证ZSet和Hash的原子更新。

方案优势

  • 性能高效:ZSet的排序时间复杂度为O(logN),适用于高并发场景。
  • 无缝兼容:无需修改Redis数据结构,复用现有ZSet特性。
  • 灵活扩展:通过调整T_maxD适应不同时间范围需求。

适用场景

  • 游戏玩家排行榜(如积分相同按最早达到时间排序)。
  • 活动实时排名(如销售额相同按订单创建时间排序)。
  • 竞赛系统(如相同得分按提交答案时间排序)。

通过复合分数编码,Redis ZSet在保证高效排序的同时,巧妙融入了时间维度,完美解决了同分按时间排序的需求。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我