redis实现一个排行榜功能

import redis import json from typing import List, Optional, Tuple from datetime import datetime

class LeaderboardService: """基于 Redis Sorted Set 的排行榜服务"""

python 复制代码
def __init__(self, redis_client: redis.Redis):
    self.r = redis_client
    self.key_prefix = "leaderboard"

def _get_key(self, board_id: str) -> str:
    """生成排行榜 key"""
    return f"{self.key_prefix}:{board_id}"

def add_score(self, board_id: str, user_id: str, score: float, 
              extra_data: dict = None) -> float:
    """
    添加/更新用户分数
    
    Args:
        board_id: 排行榜ID(如 "weekly", "global")
        user_id: 用户ID
        score: 分数(支持小数)
        extra_data: 额外数据(JSON序列化存储)
    """
    key = self._get_key(board_id)
    
    # 使用管道保证原子性
    pipe = self.r.pipeline()
    
    # 添加分数
    pipe.zadd(key, {user_id: score})
    
    # 如果有额外数据,存储到 Hash
    if extra_data:
        hash_key = f"{key}:info:{user_id}"
        pipe.hset(hash_key, mapping={
            "data": json.dumps(extra_data),
            "updated_at": datetime.now().isoformat()
        })
        # 设置过期时间(与排行榜一致)
        pipe.expire(hash_key, 86400 * 7)  # 7天
    
    pipe.execute()
    return score

def increment_score(self, board_id: str, user_id: str, 
                    delta: float) -> float:
    """
    原子增加分数(常用于实时计分)
    
    Returns:
        增加后的新分数
    """
    key = self._get_key(board_id)
    new_score = self.r.zincrby(key, delta, user_id)
    return float(new_score)

def get_top_n(self, board_id: str, n: int = 10, 
              with_scores: bool = True) -> List[dict]:
    """
    获取前 N 名
    
    Returns:
        [{"rank": 1, "user_id": "xxx", "score": 1000, "data": {...}}, ...]
    """
    key = self._get_key(board_id)
    
    # ZREVRANGE: 从高到低(默认)
    results = self.r.zrevrange(
        key, 0, n - 1, 
        withscores=with_scores
    )
    
    top_list = []
    for i, item in enumerate(results, 1):
        if with_scores:
            user_id, score = item
            user_id = user_id.decode() if isinstance(user_id, bytes) else user_id
            score = float(score)
        else:
            user_id = item.decode() if isinstance(item, bytes) else item
            score = None
        
        # 获取额外数据
        extra_data = self._get_user_extra_data(key, user_id)
        
        top_list.append({
            "rank": i,
            "user_id": user_id,
            "score": score,
            "data": extra_data
        })
    
    return top_list

def get_user_rank(self, board_id: str, user_id: str) -> Optional[dict]:
    """
    获取用户排名和分数
    
    Returns:
        None 表示不在排行榜中
    """
    key = self._get_key(board_id)
    
    # 排名(0-based,需要+1)
    rank = self.r.zrevrank(key, user_id)
    if rank is None:
        return None
    
    score = self.r.zscore(key, user_id)
    extra_data = self._get_user_extra_data(key, user_id)
    
    return {
        "rank": rank + 1,  # 转为 1-based
        "user_id": user_id,
        "score": float(score),
        "data": extra_data
    }

def get_nearby_users(self, board_id: str, user_id: str, 
                     range_size: int = 5) -> List[dict]:
    """
    获取用户附近的排名(如前5名、后5名)
    
    常用于游戏:显示"你前面还有谁"
    """
    key = self._get_key(board_id)
    
    # 获取用户排名
    user_rank = self.r.zrevrank(key, user_id)
    if user_rank is None:
        return []
    
    # 计算范围
    start = max(0, user_rank - range_size)
    end = user_rank + range_size
    
    # 获取范围内所有人
    results = self.r.zrevrange(key, start, end, withscores=True)
    
    nearby = []
    for i, (uid, score) in enumerate(results, start=start + 1):
        uid = uid.decode() if isinstance(uid, bytes) else uid
        nearby.append({
            "rank": i,
            "user_id": uid,
            "score": float(score),
            "is_current_user": uid == user_id
        })
    
    return nearby

def _get_user_extra_data(self, key: str, user_id: str) -> Optional[dict]:
    """获取用户额外数据"""
    hash_key = f"{key}:info:{user_id}"
    data = self.r.hget(hash_key, "data")
    if data:
        return json.loads(data)
    return None

def remove_user(self, board_id: str, user_id: str) -> bool:
    """从排行榜移除用户"""
    key = self._get_key(board_id)
    pipe = self.r.pipeline()
    pipe.zrem(key, user_id)
    pipe.delete(f"{key}:info:{user_id}")
    pipe.execute()
    return True

def clear_board(self, board_id: str) -> bool:
    """清空排行榜"""
    key = self._get_key(board_id)
    # 使用 SCAN 避免阻塞
    for info_key in self.r.scan_iter(match=f"{key}:info:*"):
        self.r.delete(info_key)
    self.r.delete(key)
    return True

def get_board_stats(self, board_id: str) -> dict:
    """获取排行榜统计信息"""
    key = self._get_key(board_id)
    
    total = self.r.zcard(key)
    if total == 0:
        return {"total": 0}
    
    # 获取分数范围
    min_score = self.r.zrange(key, 0, 0, withscores=True)[0][1]
    max_score = self.r.zrevrange(key, 0, 0, withscores=True)[0][1]
    
    return {
        "total": total,
        "min_score": float(min_score),
        "max_score": float(max_score),
        "avg_score": float(self.r.zscore(key, "__avg__") or 0)  # 需要额外维护
    }
相关推荐
coderlin_1 小时前
Langgraph项目三 agent搭建
java·数据库·redis
XiYang-DING2 小时前
【Java EE】锁策略、锁升级、锁消除和锁粗化
java·redis·java-ee
Devin~Y3 小时前
大厂Java面试实录:Spring Boot/Cloud + Redis/Kafka + JWT + RAG/Agent(小Y翻车版)
java·spring boot·redis·spring cloud·kafka·spring security·jwt
以为你知道啊4 小时前
mini-job极简分布式延迟任务队列 — 基于 Redis,支持 Cron 周期任务、异步协程和多执行器
redis·分布式·junit
人道领域4 小时前
【黑马点评日记】Redis分布式锁终极方案:Redisson全面解析(含源码解析)
java·数据库·redis·分布式·缓存
苏渡苇4 小时前
Redis 核心数据结构(二)——List 与消息队列
数据结构·redis·list·redis发布订阅
BullSmall4 小时前
Redis AOF 文件损坏报错:完整修复方案
数据库·redis·缓存
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.19 小时前
《redis-cluster 集群部署完全手册(含扩容+缩容)》
数据库·redis·缓存
0xDevNull20 小时前
Java项目中Redis热点Key自动检测方案详细教程
java·spring boot·redis
spencer_tseng21 小时前
redis.windows.conf 2026.04.27
windows·redis