高频面试题:如果要你找出按照点赞数量前 N 个数据,怎么做?
设计一个高性能方案, 要求 :
综合考虑可以怎么利用缓存,包括 Redis 和本地缓存。要想清楚,你这个缓存方案拿出去面试究竟有没有竞争力,有没有让面试官眼前一亮的点
允许业务折中,但是你要说清楚你准备怎么折中
ZSet 存储
我们在用户请求点赞 URL 之后, 使用 ZSET 进行维护一个点赞集合 。 使用 lua 脚本控制 check-dothing 的并发问题
如果请求的 key 不存在, 则查找数据库将已有的 value 写入到 zset 中
|
|
如果不存在那么需要把 value 写入到 zset 中
-
先判断 set-key 是否存在
-
如果存在 判断 key 是否有统计,如果有则 +1 否则 设置 value
|
|
读取
通过使用 ZRevRangeWithScores 来获取前 100 的数据进行获取 topN 的数据
|
|
优化 :
-
我们业务上只需要 top100 可以考虑只维护 前1000的数据。 使用
ZREMRANGEBYRANK删除 > 1000的内容 -
最坏情况下,无非就是冷门文章爆火,需要查一次数据库 。 但是爆火的冷门文章并发并不是很高
问题
-
前 100 名是一个高频数据
-
如果有 一亿个 数据怎么维护 ?
我们假设 100b 一条数据, 100w 条就是 100mb , 1000w 条就是 1gb . 1亿条就是 10gb 这很夸张了
前 100 名的维护
可以结合一个本地缓存,使用定时任务更新本地缓存。 例如 每 5s 调用一次 topN 函数,放进本地缓存中
本地缓存的实现
|
|
一亿个数据
-
分 key 写入 . 我们在写入数据的时候 按照每 100个元素一个。set 进行分 key 处理
-
业务折中 。 并不是真的维护一亿,而是维护近期的点赞数据 例如 3天内的 。
-
这里可以考虑算法过期 例如
likecnt / (age + 2) ^ gravity -
或者是考虑整个 key 过期
-
|
|
分key 版本的读取
每次从同一个业务中的 100 个分key 中获取前100的数据,然后再合并出前100的数据
|
|
其他
借助定时计算,每次计算1000名,使用 Zset 来维护 1000 名的分数
总结
Q : 你是如何设计一个高性能的 排行榜服务
我们使用 Zset 进行维护, 每次用户点赞的时候 会去操作一次 Zset 如果 Zset 不存在会获取数据库中的数据进行补充 。
对外提供一个 TopN 的函数用于获取 topN 的数据。
-
使用 本地缓存 + 使用定时任务每5s 维护 Zset TopN 的任务 。
-
对于大数据结构,我们采用了 分 key 的方案 , 并且考虑业务折中的方案 我们只维护最近3天的数据 。 对于超过 3 天的数据,我们会进行整 key 过期 。 如果有业务需求,可以考虑使用算法过期
-
对于数据的读取,每次获取同一个业务中 100 个key内的 前100的数据,合并后再去前 100 的数据
-
由于我们业务只需要前 100 的数据。 所以我们对于每一个 topN 维护一个 1000 数量的 Zset 。通过使用。ZREMRANGEBYRANK 进行裁剪优化
总分:62 / 100(按「2 年 Go 后端」面试深度)
能讲清 ZSet + 本地缓存 + 业务折中,达到中级入门;但并发正确性、分片收益、读路径细节经不起追问,离「眼前一亮」还有距离。
分项(供对照)
| 维度 | 分 | 说明 |
|---|---|---|
| 问题建模 | 14/20 | 点到高频读、亿级内存,缺一致性与降级 |
| 方案设计 | 16/25 | ZSet / 本地缓存 / 分片方向对,收益与边界说不清 |
| 正确性与并发 | 10/20 | Lua / 冷启动 / 裁剪与回写有明显漏洞 |
| 缓存竞争力 | 12/20 | 有本地缓存+定时刷新,缺亮点与可落地细节 |
| 表达与代码 | 10/15 | 结构尚可,代码未完成、笔误多 |
优点
- 选型合理:点赞排行用 Redis ZSet +
ZRevRangeWithScores,是面试官期望的标准答案。 - 有业务折中意识:只维护 Top1000、近 3 天、时间衰减公式,比死磕「全量精确」更像有线上经验。
- 意识到读热点:Top100 用本地缓存 + 定时刷新,方向正确。
- 有规模感:用「100B × 条数 ≈ 内存」估算,说明想过容量,而不是只背 API。
- 分片合并思路:每分片取 TopK 再归并,全局 Top100 在理论上是成立的(全局 Top100 一定落在各分片 Top100 里)。
缺点(面试深挖会挂的点)
1. 写路径 / Lua 正确性不足
- 第一段脚本:member 不存在就
return 0,和「key 不存在回源 DB」混在一起,边界说不清。 EXISTS+ 回源 +ZADD有典型竞态:多请求同时发现 key 不存在,会重复回源、互相覆盖。- 直接
ZINCRBY通常更简单;自己做ZSCORE+ZADD却没讲清为何不用原子自增。 - 冷门突爆「并发不高」不能当结论,面试官会问:缓存击穿 / 单飞 / 互斥回源怎么做?
2. 分 Key 没有真正解决「10GB」问题
- 分 100 个 key 只是摊内存到多实例/多槽,总量仍约 10GB,除非配合「每分片只留 Top1000 + 时间窗」。
- 文档里「分 key」和「裁剪 1000 / 只保 3 天」缠在一起,容易被问成:分片到底解决什么?裁剪丢了的 member 再点赞怎么回写?
3. 读路径代码与叙述不一致
|
|
- 100 次串行
ZRevRange,没提 Pipeline / 并发,高 QPS 下这是减分项。 h.Write([]byte(bizId)):bizId是int64,这样写编译都过不了,暴露准备粗糙。
4. 「眼前一亮」的缓存方案偏薄
- 本地缓存只给了结构体字段,没有:过期策略、失败降级、单飞、多实例一致性、Redis 挂了怎么办。
- 5s 定时刷新可以,但要主动说清楚:最多 5s 脏读、是否可接受、写后是否主动失效。
5. 总结段有自相矛盾
- 一边「只维护近 3 天」,一边「每个 topN 维护 1000 的 ZSet 裁剪」——没串成一条清晰主路径(写什么、裁什么、读什么、何时过期)。
建议(按面试表述优先级改)
先定一条主链路(30 秒版本)
点赞写:
ZINCRBY(或 Lua 原子自增)→ 可选裁剪到 TopK → 读:本地缓存 Top100(定时/主动刷新)→ Redis 兜底 → 业务只保证近 N 天、允许秒级延迟。
补齐会被追问的 5 个点
- 冷启动:key 不存在时 singleflight / 分布式锁回源,防止击穿。
- 与点赞主链路一致性:双写失败怎么补偿(异步对账 / 消息队列重放)。
- 分片收益:说清是「内存分散 + 裁剪」还是「近似排行」;读用 Pipeline。
- 本地缓存:成功/失败 TTL、刷新抖动、降级返回旧数据。
- 降级:排行挂了不影响点赞主流程(熔断、开关、返回空或历史快照)。
代码准备
- 把
LikeTopV1补完:归并堆 /sort+ 截断 Top100。 - 分片 key 用
strconv.FormatInt;Lua 收敛成一种语义讲透。 - 修标题笔误
signle→single,避免细节减分。
想加分的「亮点」(选 1~2 个讲深)
- 读写分离:写只碰分片 ZSet,读只碰「聚合后的 Top100 专用 key」(定时任务算好写入),线上读路径 O(1)。
- 或:消息异步更新排行,点赞接口不直连大 ZSet,用最终一致性换写吞吐。
面试官一句话结论
作为 2 年经验的准备稿:骨架有了(约及格偏上),深度和正确性不够。把「并发回源、分片真实收益、读合并实现、一致性与降级」补成能闭环口述的一条故事,分数可以到 75~80;再补上「聚合 Top key / 异步更新」一类可落地亮点,才接近「眼前一亮」。