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)  # 需要额外维护
    }
相关推荐
一个有温度的技术博主3 小时前
Redis系列三:在linux上安装Redis
linux·数据库·redis
changhong19863 小时前
redis批量删除namespace下的数据
数据库·redis·缓存
**蓝桉**4 小时前
Grafana Redis 监控面板全解析(小白版)
redis·prometheus
H_老邪4 小时前
redis 安装
数据库·redis·缓存
霖霖总总4 小时前
[Redis小技巧17]深入解析 Redis 缓存穿透:原理、防御策略与布隆过滤器实践
数据库·redis·缓存
白太岁5 小时前
Redis:缓存、集群、优化与数据结构
redis·后端
星辰_mya5 小时前
Redlock 算法:是分布式锁的“圣杯”还是“鸡肋”
jvm·redis·分布式·面试·redlock
霖霖总总5 小时前
[Redis小技巧16]Redis 安全加固与加密传输指南:从基础到高级策略
数据库·redis
四谎真好看5 小时前
Redis学习笔记(实战篇2)
redis·笔记·学习·学习笔记