ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Redis ZSet的500万用户评分排行榜系统设计

基于Redis ZSet的500万用户评分排行榜系统设计 想把一个评分排行系统做到 500 万用户规模还能稳定跑最值得看的不是某个框架有多强而是实时性定义、存储选型和压测方法是不是提前对齐。平时做活动榜单、积分榜、直播互动榜、内部评级榜最后踩来踩去基本都是这几个核心问题同分怎么排、缓存怎么刷、热点 key 怎么扛、数据不一致怎么补偿。这篇文章按我自己的落地顺序来写先判断业务要什么再本地跑通最小原型然后说清楚从几万用户到几百万用户要改哪些设计最后给出压测指标、常见坑和一张可直接照抄的检查清单。适合后端开发、技术负责人以及正在做排行榜或积分模块但没有完整经历过高并发阶段的人。标题里的 rating 就是评分、排行、评级体系500 万则代表一个常见的大型活动或长线业务的用户规模。不要被“高并发”“大流量”这些词吓住先想清楚一个基础问题你的榜单允许延迟多久。1. 先定义榜单类型实时、准实时还是离线1.1 三种榜单各自的业务要求实时榜单用户能刷到自己的名次前三名变化最好秒级反馈。典型场景是直播小时榜、竞猜榜、抢购活动榜。准实时榜单不需要秒级但希望在一分钟内看到结果。典型场景是日榜、周榜、投票活动榜。离线榜单统计完成后生成延迟可以是几分钟甚至几小时。典型场景是月度绩效评比、月度活跃排名、运营复盘报表。很多团队拿到需求后第一反应是直接做 Redis 排行这是顺序错误。先确认榜单类型因为不同类型对应的存储、写入方式和一致性要求完全不同。1.2 判断标准实时性、一致性和实现成本判断标准可以列成一张表格维度实时榜准实时榜离线榜展示延迟秒级30 秒到分钟级分钟级以上数据一致性最终一致可容忍最终一致可容忍更容易做到强一致存储推荐Redis 有序集合Redis 加定时落库离线分析或仓库实现复杂度高中低典型场景直播互动榜每日消费榜月度报表这里为什么要把类型放在最前面因为后面所有技术选型都依赖这个答案。如果业务只需要一小时刷一次的周榜你没必要为 Redis 集群和缓存一致性投入太多如果是实时榜就要从写入、查询、降级一条链路都做好准备。另外一个容易被忽略的点业务方说的“实时”往往不是真正的实时而是“看起来实时”。所以开发前要确认可接受的延迟窗口。这个时间会直接决定你是否需要引入消息队列、聚合任务和缓存预热。1.3 先确认分数规则和数据量级在开始设计前还需要确认几个数据问题参与用户总量级是 5 万、50 万还是 500 万。单个用户会产生多少条加分记录一次还是一天内多次。加分因子是什么比如打赏金额、点赞数、浏览时长等。分数可以回退吗比如用户退款、取消操作导致扣分。榜单周期结束后前一天的数据去哪里是归档还是继续累加。这些规则看起来像运营问题但最终都会变成技术成本。比如分数可回退意味着缓存里不能只存最终值还要有扣分记录和异常处理路径比如 500 万用户每个用户每天产生多条记录写入链路就不能简单写成一条 SQL 插入要考虑批量聚合。2. 本地先跑通一个最小可用的排行榜服务2.1 为什么优先用 Redis 的有序集合做排行榜我一般会优先考虑 Redis 的 ZSet也就是有序集合。原因是它天然支持下面这些操作给某个成员增加分数ZINCRBY。查询某个成员的当前分数ZSCORE。查询某个成员当前排名ZRANK。按分数从高到低取前 N 名ZREVRANGE。这些操作在单个 key 上都是高效操作非常适合排行榜场景。和关系型数据库频繁执行 ORDER BY 相比ZSet 把排序结果提前维护在内存里读取时不需要临时计算。2.2 本地环境准备本地验证时不需要一上来就上集群。以常见情况为例建议准备Linux、macOS或者 Windows 上的 WSL 环境。安装 Redis并确认 redis-cli 能连通。安装 Python 3 以及 redis-py。安装依赖时不需要纠结具体版本以你自己环境的可用版本为准。# 以 Ubuntu/Debian 系为例 sudo apt update sudo apt install redis-server python3 python3-pip pip install redis启动 Redis 后先用 redis-cli 确认连接正常redis-cli ping如果返回 PONG说明服务正常。这里最容易出的问题有几种Redis 没启动、端口被占用、Windows 下没有安装成服务。建议先解决最基本的连通性问题再做业务验证。2.3 用一段 Python 示例跑通加分和榜单查询下面是一段精简示例用来验证核心逻辑。注意它不代表生产实现只是为了让你理解整个操作的顺序。import redis r redis.Redis(host127.0.0.1, port6379, db0) RATING_KEY rating:activity:2025 # 模拟用户加分 users [ (user_1001, 150), (user_1002, 320), (user_1003, 89), (user_1001, 200), # 同一用户再次加分 ] for uid, score in users: r.zincrby(RATING_KEY, score, uid) # 查询每个用户当前分数 for uid, _ in users: print(uid, r.zscore(RATING_KEY, uid)) # 查询当前前 3 名 top3 r.zrevrange(RATING_KEY, 0, 2, withscoresTrue) for rank, (uid, score) in enumerate(top3, start1): print(rank:, rank, uid, score)这段代码的逻辑很简单先给用户加分然后查询分数再取前 N 名。重点不是代码本身而是通过它理解 ZSet 的操作顺序。你先验证这一步再考虑批量写入和缓存。2.4 单机验证成功标准至少要检查四个结果同一个人多次加分后分数累计正确。按分数从高到低取前 N 名顺序没有错误。分数相同时Redis 默认按字典序排序这个可能需要后端二次排序。分数能正常读取不会因为类型问题导致查询失败。如果前三点都正常说明最小原型已经成立。接下来真正的工作是围绕这个原型做精细化设计。3. 从几万用户到 500 万规模真正要改的是四层设计3.1 数据层先做内存和容量估算很多团队在用户量只有几万时直接用单个 Redis 的 ZSet 存全量效果很好。但当用户量到百万级甚至 500 万级时首先要算的是内存放不放得下。ZSet 在 Redis 里的存储开销包含成员名、分数、字典和跳表节点。常见情况下一个活跃用户成员大约会占几十字节到几百字节不等具体取决于 key 长度和成员内容。用保守方式估算500 万成员的有序集合内存很容易到 1GB 以上。这还只是一个榜单 key。如果活动有多个榜单、多个周期内存会成倍增长。这里的判断标准不是“我服务器内存大没事”而是要提前知道 Redis 内存上涨的斜率。否则上线后每多一个活动 key都会对整体内存造成冲击。如果单 key 内存压力大常见方案有几种按活动 ID 拆分 key。按榜单周期拆分 key例如每日一个 key。只把 TOP N 结果放 Redis全量数据落库。在数据库里做聚合任务定期生成榜单结果。3.2 写入层高频加分场景要做聚合和削峰小流量时每个用户加分直接写 Redis 没问题。但 500 万用户集中参与时单个 Redis 写入 QPS 会很高。更常见的问题是网络连接数和多次写入带来的 CPU 浪费。我一般建议这样做客户端先记录原始行为写入消息队列或本地缓冲。聚合任务每隔几秒按用户维度做一次汇总。汇总结果批量更新 ZSet。定期把结果持久化到数据库便于回放和校验。这样做的原因不是 Redis 扛不住一次写入而是高频小写入会造成大量网络开销和线程切换。批量更新后同样多的业务行为写入 Redis 的次数能少一个数量级。这里要注意一个边界如果业务要求实时性极高聚合窗口不能太长。可以根据业务可接受的延迟设置 2 到 5 秒的聚合窗口或者用更轻量的局部更新方案。聚合窗口越长Redis 压力越小但榜单延迟越明显。3.3 查询层榜单缓存和热 key 处理排行榜查询和普通数据查询不同大部分人只会看前 10 名或前 100 名而不是随机翻 500 万个名次。所以实际读取压力高度集中在前端的少量成员上。应对方式将前 100 名结果缓存到进程内或 Redis设置较短过期时间。每次写操作后不一定立即刷新榜单缓存可以等几秒后再刷新。对第 1 名这种特定 key 的热点请求做额外缓存。接口层加限流避免单个用户频繁刷新被误判为攻击。热 key 问题的本质是“同一时间大量请求读同一个 key”。解决方向不是让单个 key 无限扛流量而是让绝大多数请求命中更上层的缓存并让过期时间带有随机性避免所有缓存同时失效。3.4 一致性层补偿和核对机制写入 Redis 的分数理论上都应该能回溯到原始行为记录。否则一旦出现数据错乱、缓存丢失、任务并发覆盖你很难恢复。生产环境至少要有一条对账链路原始行为记录写到消息队列或日志表。聚合任务生成每个周期的汇总值。每天跑一次核对任务比较汇总值和 Redis 值。发现差异时使用汇总值重建对应榜单 key。这步不能省。500 万用户的榜单错误不是出现在功能测试阶段而是在线上活动期间突然被发现。到那时候能依靠的不是运气而是原始数据和重建流程。4. 压测与稳定性验证比 QPS 更值得关注的指标4.1 压测从哪里开始不要一开始就模拟 500 万用户。建议从最小场景开始先用 100 个并发用户连续写入和查询。逐步提升到 1000、5000 并发。同时开启缓存命中统计和 Redis 慢日志。让压测至少持续 10 到 15 分钟观察长时间运行是否稳定。压测工具有很多可以用 JMeter、wrk、k6 或者 Locust。选哪个不是重点重点是要模拟两个场景写入场景用户不断加分。查询场景大量用户读取 TOP N 榜单。很多系统单独看写入没问题单独看查询也没问题混合场景一开就出错。因为写操作导致缓存失效查询量会同时压在数据库或 Redis 上之前没有暴露的问题就会集中出现。4.2 关键指标怎么判断常见判断指标包括指标判断思路平均响应时间和业务预期对比不代表真实体感p99 响应时间更接近用户真实体验重点观察错误率不只看网络问题要区分 4xx / 5xx / 超时吞吐量 QPS和系统容量对比而不是单纯追求越大越好CPU / 内存看是否有明显突刺Redis 慢日志看是否存在大量慢查询客户端连接数看是否超过 Redis 配置上限这里最容易踩的坑是只汇报 QPS不看 p99。比如平均 10ms但 p99 到 2 秒说明一部分用户已经明显卡顿。p99 的变化趋势比平均值更能反映稳定性。4.3 问题排查顺序如果压测过程中出现报错或延迟升高我会按下面的顺序排查先看现象是写入失败还是查询超时还是 Redis 连接被拒绝。再看 Redis 状态内存、连接数、慢日志、key 数量。再看应用日志是否有异常堆栈、超时时间和重试逻辑是否正确。再看网络是否存在频繁断开、带宽跑满。最后看参数并发数、连接池大小、超时时间、缓存过期时间是否合理。不要一上来就改代码。很多问题不是代码 bug而是 Redis maxclients 默认连接数不够、连接池设置过小、缓存穿透打到数据库导致锁竞争。先看资源层再看代码层。5. 上线后容易踩的边界坑5.1 同分排序不稳定ZSet 在分数相同的情况下默认按成员名字典序排序。这个顺序在不同 Redis 版本下可能不是业务想要的结果。如果业务要求同时长、同时分时先完成的人排名靠前就需要额外字段或者用秒级时间戳拼成浮点分数。不要以为这是个边角问题。活动截止前最后几分钟大量用户会达到相同分数榜单前几名会反复跳动。如果排名规则没有提前约定后续用户投诉会很难处理。5.2 周期切换和数据升级定时榜在周期切换瞬间会出现新榜为空、旧榜还没归档的问题。建议提前准备两个 key预先创建下一周期的 key并在切换时通过原子操作重命名或切换读取标记。同时要清理旧 key 的过期时间避免内存一直被占用。5.3 防刷和异常行为评分排行系统天生容易被刷。一个用户可以通过大量小号、大量操作把自己的分数刷到不正常水平。技术上能做的至少包括按用户维度做频率限制。按设备、IP、账号等级做风控标记。对异常增长的值单独告警。活动结束后做人工抽检和回滚。防刷不能完全靠技术需要业务规则辅助。但如果在设计阶段没有留出“记录原始行为”的环节事后就失去了排查依据。5.4 降级方案一旦 Redis 出问题不能让整个业务直接不可用。至少准备降级开关榜单接口返回缓存中的旧快照并在页面上提示“榜单延迟更新”。写入操作暂时进入本地缓冲或消息队列Redis 恢复后补写。数据库只读兜底查询慢也总比白屏强。降级是防御性设计不是核心功能。但上线前一定要演练一次否则真到高峰期开关在哪、怎么关、关闭后谁负责恢复都会变成混乱点。6. 一个可以直接照着做的落地检查清单6.1 设计阶段确认榜单类型实时、准实时、离线。确认积分规则加分、扣分、回退场景。估算数据量注册用户、活跃用户、总分操作数。确定是否需要归档历史榜单。6.2 开发阶段使用 Redis ZSet 完成核心排序操作。高频写入走聚合和批量更新。榜单查询结果做多级缓存。保留原始行为记录支持数据重建。6.3 验证阶段单机原型验证加分、排名、同分排序。压测场景写入、查询、混合场景至少跑 15 分钟。观察 p99、错误率、Redis 慢日志。模拟 Redis 故障验证降级开关。6.4 上线后检查检查项频率重点关注Redis 内存增长每小时是否有异常 keyRedis 慢日志每小时大量慢查询需及时处理榜单缓存命中率每半小时命中率过低则调整过期时间对账任务每日原始数据和 Redis 值是否一致异常数据告警实时单用户突增、异常访问频次这张表不需要所有团队都一样但至少要把 Redis 内存、慢日志和对账任务放在监控里。项目真正跑到 500 万规模时最贵的不是服务器而是出问题后排查的时间。如果只让我留一条经验无论榜单多简单都要保留原始行为记录并让数据可以被重建。因为评分排行系统的所有展示最终都依赖数据的可靠性和可解释性。把这个基础打牢后面加缓存、加聚合、加降级方案都会顺很多。
返回列表