商家想要知道自己店铺卖的最好的 top 50 商品,如何实现这个功能?
- 标签:后端、系统设计、场景题
🏛 业务架构师
商家店铺热销 Top50 商品,这题的关键判断是:排行榜是"读密集 + 允许分钟级延迟"的场景,绝不能每次查询现场聚合百万订单。我按量级分档。
第一档:中小商家量级(日订单几千到几万)——定时聚合
- 订单事件(或 binlog)驱动计数:每笔成交
INCR sales:count:{shopId}:{skuId}(Redis) - 定时任务(如每小时)把店铺商品按销量快照落库/写缓存
top50:{shopId},商家后台直接读现成榜单 - 成本极低,商家看榜单的需求容忍分钟级甚至小时级延迟——商家要的是"经营参考"不是实时行情
第二档:平台级实时榜单(大促实时热销榜)
- Flink 消费订单流,窗口聚合(如 1 小时滑动窗口),增量维护 TopN(堆结构),结果写 Redis
- 这套的成本(实时计算集群)只有大促作战、C 端展示场景才值得
必须主动提的业务设计点(架构师视角)
- 口径定义先和业务确认:销量按"支付成功"还是"剔除退款"?按自然月还是滚动 30 天?口径不清的榜单会引发商家客诉——榜单是商家信任的产品,错一次口碑就塌
- 防刷:刷单会污染榜单,聚合前要过风控标记(剔除可疑订单),否则榜单变成刷单激励工具
- 下架/售罄商品是否保留在榜,展示逻辑和运营策略要对齐
- 长尾商家优化:Top50 需要全量商品排序,但多数小商家 SKU 数不到 50——聚合时直接全量快照即可,根本不需要堆算法;头部大商家(10 万 SKU)才需要真正的 TopN 维护
收尾:TopN 类需求我的固定三问:口径是什么、延迟容忍多少、要不要防刷——答完这三个,方案自然浮出。
🏗 系统架构师
Top N 排行榜是典型的聚合与实时性权衡题,我按商家视角拆需求:粒度(实时/日/月/年)、数据量、访问量。
方案一:Redis ZSet(实时榜,首选解释) 订单支付成功后 ZINCRBY shop:{shopId}:goods:hot 1 {skuId},查询 ZREVRANGE ... 0 49 WITHSCORES 直出 top50。单店铺商品数一般 < 1 万,ZSet 完全放得下。实时性好,写入是 O(logN)。
方案二:预计算 + 数仓 T+1(精确榜) "卖得最好"如果指销量/销售额的日榜月榜,真实链路是:订单 binlog → 数仓(离线或实时)按 shop_id + sku_id 聚合 → 结果回写 MySQL/Redis,接口只读结果。理由:订单量千万级/天,在业务库上聚合是灾难;数仓天然处理迟到数据、退款冲减。
方案三:流式实时聚合(分钟级延迟) Flink 消费订单流,按窗口(如 1 小时滚动)聚合出 top50,写入 Redis,查询端读最新窗口。大促实时榜单就这么做。
我的实际选型:普通商家后台用方案二 T+1(数据准确、成本低),页面展示"截至昨日";营销活动大屏用方案三。方案一的 ZSet 适合"浏览量/点赞"这类轻量热度。
易漏的细节:退款要冲减;"最好"要和业务定义对齐(销量 or GMV);排行榜查询加本地缓存(1~5 分钟 TTL)扛商家反复刷新;冷门店铺也要有默认兜底逻辑。
💻 全栈开发专家
Top 榜单本质是排行榜问题,我的方案按数据量分级。
方案:MySQL 聚合表 + Redis ZSET(主流做法)
- 销量累计:订单支付成功后实时更新:
bash
ZINCRBY shop:{shopId}:goods:rank 1 "sku:10001"原子自增,天然有序。或者更稳的做法:订单消息异步消费后更新,削峰。
- 取 Top50:
bash
ZREVRANGE shop:{shopId}:goods:rank 0 49 WITHSCORESO(logN) 搞定,毫秒级。
- 持久化兜底:ZSET 是缓存,需要落库防线:
- 每日销量表
daily(sku_id, date, qty),唯一键(sku_id, date),ON DUPLICATE KEY UPDATE qty = qty + ?,实时或小时级汇总。 - XXL-JOB 定时把 ZSET 刷回 MySQL/重建 ZSET,Redis 挂了能重建。
- 每日销量表
细节与坑
- 口径要问清:按天/月/总销量?面试时主动确认,体现严谨。
- 并列排名:ZSET 同分按 member 字典序,展示层若要求同分并列要处理。
- 大商户热点:单 key 不会太大(一个店 SKU 数有限),但促销秒杀时更新 QPS 高,可改"本地累加 + 定时批量 ZINCRBY"降低 Redis 压力。
- 数据量再大(平台级榜单)就走离线计算:Flink 实时聚合或 Spark 小时级批量,结果写回 Redis,在线只做读取。