惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
有赞技术团队
有赞技术团队
aimingoo的专栏
aimingoo的专栏
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
L
LangChain Blog
Blog — PlanetScale
Blog — PlanetScale
阮一峰的网络日志
阮一峰的网络日志
Microsoft Azure Blog
Microsoft Azure Blog
云风的 BLOG
云风的 BLOG
Google DeepMind News
Google DeepMind News
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
Hugging Face - Blog
Hugging Face - Blog
Y
Y Combinator Blog
D
DataBreaches.Net
Engineering at Meta
Engineering at Meta
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements

Aimee's Blog

秒杀怎么防机器人:验证码之外的几道防线 防薅羊毛怎么做:号码风控、设备指纹与分层策略 接口限流怎么选:固定窗口、滑动窗口、令牌桶 批量打包下载怎么设计:流式 ZIP 与异步任务 图片处理怎么做:异步处理与 CDN 实时参数 大文件上传怎么设计:分片、断点续传、秒传 报表可重跑的设计:快照、水位线与幂等 UV 统计为什么有误差:精确计数与近似计数怎么选 大数据量导出怎么做:流式写入与异步任务 排行榜的两类需求,难度差了一个数量级 内容审核怎么选:先审后发,还是先发后审 互关状态怎么保持同步:双向关系的维护与缓存失效 并发点赞的计数设计:原子操作与最终对账 删了父评论,子评论怎么处理——先想清楚用户看到的是什么 定时消息任务触发了两次 消息量大,写扩散还是读扩散——接到这个需求先问清楚规模 同一条消息推送了三次 活动结束了,用户还在收短信 接到注销需求,先问两个问题 微服务与服务拆分:何时拆、怎么拆 异步与事件驱动架构:把协作从「打电话」改成「发消息」 高可用设计:怎么让系统尽量不宕机 可扩展性设计:怎么让系统加机器就能扛更多 缓存架构:多级缓存怎么搭 高并发三板斧:限流、熔断、降级 架构设计到底在设计什么 —— 从单体到微服务的演进 服务成本账:一个服务一个月烧多少钱 可观测性:线上出问题怎么查 API 设计:好接口长什么样 消息队列:为什么要 MQ,以及丢失、重复、顺序怎么破
实时热榜怎么设计:Redis ZSet 与热度分值
Aimee · 2026-08-08 · via Aimee's Blog

实时热榜怎么设计:Redis ZSet 与热度分值

接到需求:展示实时热榜,按热度降序,支持分页,每分钟刷新。

第一反应:SELECT id, score FROM post ORDER BY score DESC LIMIT 20,不就完了?

但数据量和并发一上来,这条 SQL 很容易变成全站最慢的查询之一。


为什么 ORDER BY score 很慢

数据量到百万行时,ORDER BY score 即使有索引,每次请求都要走索引扫描 + 回表,结果集大、排序代价高。

更致命的是:热榜查询通常是高并发场景,每个用户刷新都打一次 DB,几百 QPS 都扛不住。


Redis Sorted Set:为排名而生

Redis Sorted Set(ZSet)是专门为排名场景设计的数据结构:

  • 每个成员带一个 score(浮点数)
  • 天然按 score 排序
  • 范围查询 O(log N)
  • 取前 N 名:ZREVRANGEZREVRANGEBYSCORE
// 帖子热度更新(每次点赞/评论/分享时调用)
redis.zadd("hot_rank", newScore, String.valueOf(postId));

// 查热榜前 20
Set<ZSetOperations.TypedTuple<String>> top20 = 
    redis.zrevrangeWithScores("hot_rank", 0, 19);

读热榜不走 DB,全在内存里,毫秒级响应,QPS 轻松上万。


热度分如何计算

热度分不只是点赞数,通常是一个加权公式:

score = likeCount * w1 + commentCount * w2 + shareCount * w3 + decayFactor(time)

时间衰减确保老内容不会永远霸榜。常见衰减方式:

// Hacker News 风格的时间衰减
double score = (likeCount + commentCount * 2) / Math.pow(ageInHours + 2, 1.5);

这个分不是静态的,每次互动都要重算并更新 ZSet:

public void onLike(long postId) {
    double newScore = calcScore(postId);  // 重新计算热度分
    redis.zadd("hot_rank", newScore, String.valueOf(postId));
}

实时更新 vs 定时刷新

实时更新:每次互动(点赞、评论)立刻更新 ZSet score。

  • 优点:热榜数据实时
  • 缺点:热门内容每秒几十次更新,ZSet 写入压力大;如果要全局重算 score(带时间衰减),实时无法做到

定时刷新:定时任务(如每分钟)批量重算所有活跃内容的 score,整批写入 ZSet。

  • 优点:计算解耦,支持复杂分值公式,热榜对外表现稳定
  • 缺点:最多延迟一个刷新周期

实际系统通常组合:互动事件实时更新 ZSet(ZINCRBY 累加点击数),另有定时任务按完整公式(含时间衰减)重算 score 并覆盖写入。


榜单分区

不同维度的榜需要不同的 ZSet key:

hot_rank:global          -- 全站热榜
hot_rank:cat:tech        -- 科技分类热榜
hot_rank:region:beijing  -- 北京地区热榜

榜单多了,ZSet 的总内存会增长。每个榜只保留前 N 名(如 1000),定期用 ZREMRANGEBYRANK 清理长尾:

redis.zremrangeByRank("hot_rank:global", 0, -(1001));  // 只保留前 1000

热榜看起来是查询问题,实际是缓存更新问题。ZSet 让读变快只是第一步,分值设计和更新策略才是让榜单"好用"的关键。