电商排行榜轻量化方案:Redis今日销量阈值

一、方案背景与痛点

电商商品排行榜(7 日销量榜、当日日榜、热度榜)是电商核心流量入口,绝大多数开源 / 自研方案存在严重两极化问题:

1. 传统全量 MySQL 计算方案(性能

  • 每天 / 每小时全量扫描几十万 SPU + 大表 JOIN
  • 数据库 CPU 抖动、慢 SQL 堆积、从库压力爆表
  • 99% 冷门商品 0 销量,完全没必要参与计算,但依然被扫描

2. 纯 Redis 全量 ZSet 方案(稳定性

  • 所有商品塞入 ZSet,成员几十万,内存爆炸
  • ZUNION、遍历、分数衰减极易阻塞 Redis 单线程
  • 冷热商品混在一起,头部更新慢、尾部无效占用资源

3. 复杂分片方案(太重、小团队无法落地)

  • Kafka 集群、小时分片 ZSet、多消费组、分库分表
  • 架构重、维护成本高、中小团队 hold 不住

基于以上痛点,本文落地一套「轻量化、高性能、高稳定、极易落地」的工业级排行方案

核心原创思路 :只有今日销量达到阈值的商品,才允许进入 TOP 榜单 ZSet,冷门全程不计算、不进缓存、不耗性能;同时拆分当日销量日榜7 日综合热度榜两套独立榜单,解决零点切换空白、新开盘数据稀少问题。

二、本方案核心设计理念

1. 核心思路

  • 冷门商品:只 Redis 计数,不参与排行、不进 ZSet、不查库
  • 潜力黑马:今日销量达标阈值,才触发完整分数计算 + 冲榜比对
  • 头部爆款:常驻 ZSet,实时增量更新排名,豁免销量门槛
  • 数据真值:以 MySQL 汇总表为准,Redis 只做实时展示与准入判断
  • 双榜单隔离:7 日综合榜长期稳定;当日日榜双 key 轮换兜底,杜绝页面空白

2. 方案优势

✅ 彻底无全表扫描:只计算有动销、达门槛的商品

✅ Redis 内存极小:ZSet 只留存有效头部商品

✅ 黑马实时上榜:新品爆单无需等待凌晨刷新

✅ 数据库压力极低:95% 商品不走聚合计算

✅ 架构极简:无需小时分片 ZSet、无需复杂合并

✅ 容错极强:Redis 丢数据可秒级重建

✅ 页面零空白:当日榜单量不足自动降级复用昨日榜单

三、整体架构分层(最简四层)

1. 采集层(异步解耦,不卡下单主链路)

订单支付、退款、取消订单 → MQ (RocketMQ/Kafka) 异步投递

主业务链路零 Redis、零 DB 排行计算逻辑,保证下单 RT 最低。

2. 实时计数层(Redis 今日销量计数)

采用 String 结构单商品独立计数:

  • 今日单品销量 Key:rank:sale:today:{spuId}
  • 今日动销白名单:rank:sale:today:whitelist(Set 结构,记录今天有成交的所有商品,避免全量 Scan)
    所有成交商品仅做两件事:计数累加 + 加入白名单。不达阈值完全不参与后续任何排行计算。

3. 阈值准入层(核心精髓)

后台可动态配置阶梯阈值:

  • 常态白天模式:普通商品冲榜阈值 = 20 单,≥20 单才允许写入榜单 ZSet
  • 凌晨低谷模式(0:00-8:00):阈值自动下调至 5 单,适配低流量时段
  • 头部爆款白名单、15 天扶持新品:无销量门槛,成交 1 单即可更新榜单

4. 基准校准层(兜底保精准)

  • 小时级:DB 修正 Redis 计数偏差,防止消息丢包、退款导致分数漂移
  • 零点级:归档单日销量,轮换当日榜单新旧 key
  • 凌晨级:全量动销商品重算 7 日总分,整体覆盖刷新 7 日综合榜单

四、数据结构设计(两张 MySQL 表 + 全套 Redis 键)

1. MySQL 核心汇总表(唯一真值来源)

(1)小时销量预汇总表(规避原始订单大表 JOIN)

|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sql CREATE TABLE order_hour_spu ( spu_id BIGINT NOT NULL, stat_hour DATETIME NOT NULL, sale_num BIGINT DEFAULT 0, sale_amount DECIMAL(18,2) DEFAULT 0, PRIMARY KEY (spu_id,stat_hour) ) ENGINE=InnoDB; |

特点:无动销无数据,天然过滤 90% 滞销商品。

(2)排行基准分数表

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| sql CREATE TABLE goods_rank_total ( spu_id BIGINT NOT NULL, rank_type TINYINT NOT NULL COMMENT '1=7日热度 2=当日销量', period TINYINT NOT NULL, score DECIMAL(18,4) NOT NULL DEFAULT 0, seven_sale_num BIGINT DEFAULT 0, update_time DATETIME DEFAULT NOW() ON UPDATE NOW(), UNIQUE KEY uk_type_period_spu(rank_type,period,spu_id) ); |

2. Redis 全套键划分

① 7 日综合热度榜(首页核心展示,零点永久不清空)

|-----------------------------------------------------------------------------------------------------------------------------------------|
| Plain Text rank:hot:7day:top # 常驻7日TOP2000榜单ZSet rank:hot:7day:snap:yyyyMMdd # 每日榜单快照,异常回滚用 rank:hot:white_list # S/A头部爆款白名单Set,豁免20单门槛 |

② 当日销量日榜(独立 Tab 页,双 key 轮换防空白)

|------------------------------------------------------------------------------------------|
| Plain Text rank:sale:today:curr # 当日实时榜单ZSet rank:sale:today:last # 昨日备份榜单ZSet,数据不足时兜底展示 |

③ 实时计数层

|------------------------------------------------------------------------------------------|
| Plain Text rank:sale:today:{spuId} # 单品今日实时销量String rank:sale:today:whitelist # 今日成交商品集合 |

五、完整执行链路

步骤 1:订单异步计数(日间实时)

支付成功 MQ 消费流程:

  1. 幂等判断 orderId 是否已处理,防止重复加减
  2. Redis INCRBY rank:sale:today:{spuId} 1
  3. SADD rank:sale:today:whitelist {spuId}
  4. 同步 UPSERT 写入 order_hour_spu 数据库落真值

退款 / 取消订单:执行DECRBY反向扣减销量,数值归零后同步从当日 ZSet 移除该商品。

步骤 2:阈值判断与双榜单写入逻辑

读取商品当日实时销量,分三类处理:

  1. 白名单爆款 / 15 天新品:无门槛,任意销量立刻计算分数,同步更新 7 日总榜 + 当日 curr 榜单
  2. 普通商品 销量≥阈值(20/5 动态切换):查询 MySQL 完整分数,执行冲榜对比逻辑
  3. 普通商品 销量<阈值:仅计数,不操作任何 ZSet 榜单

步骤 3:冲榜 Lua 原子逻辑(无并发错乱)

  1. 商品已在 ZSet:直接 ZADD 覆盖最新分数,实时更新排名
  2. 商品不在 ZSet:读取榜单末尾最低分对比
  • 新分数更高:淘汰末位商品,插入当前商品
  • 分数不足:仅保存 MySQL 分数,不占用 Redis 内存

步骤 4:零点平滑切换(核心解决当日榜单空白)

零点不删除 7 日总榜,只处理当日计数与当日榜单轮换:

  1. 读取rank:sale:today:whitelist全部 SPU,批量归档销量写入 order_day_spu 日汇总表;
  2. 当日榜单重命名轮换:RENAME rank:sale:today:curr rank:sale:today:last,昨日榜单完整留存做兜底;
  3. 初始化空集合rank:sale:today:curr作为新一天当日榜单容器;
  4. 批量删除所有rank:sale:today:{spuId}单品计数 key、清空白名单;
  5. 复制rank:hot:7day:top生成当日 7 日榜单快照;
  6. 自动切换凌晨低谷阈值(5 单),放宽当日榜单准入条件。

|----------------------------------------|
| 重点:7 日综合榜单全程保留不动,页面首页永远有完整排行,不受零点重置影响。 |

步骤 5:当日榜单数据不足兜底逻辑(接口层强制兼容)

页面请求「今日热销」Tab 时,后端统一判断:

  1. 先查询当日实时榜单商品总数 ZCARD rank:sale:today:curr
  2. 配置最低安全阈值(示例:少于 10 条视为数据不足)
  3. 数据充足:正常返回rank:sale:today:curr实时今日排名
  4. 数据不足(凌晨新开盘、全天低成交):自动降级返回rank:sale:today:last昨日完整榜单填充页面,用户无空白感知

伪代码示例:

|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| java // 查询今日榜单数量 Long todaySize = redis.zcard("rank:sale:today:curr"); String targetKey = "rank:sale:today:curr"; // 不足10条,切换昨日榜单兜底 if (todaySize < 10) { targetKey = "rank:sale:today:last"; } // 返回前N条排行数据 return redis.zrevrangeWithScores(targetKey, 0, 49); |

步骤 6:凌晨 2 点全局校准(终极数据兜底)

  1. 仅遍历近 7 天 order_day_spu 中有成交记录的动销商品,绝不扫描全量 SPU;
  2. 批量计算 7 日完整加权热度分数,批量更新 goods_rank_total 基准表;
  3. 内存排序截取 TOP2000,全覆盖刷新rank:hot:7day:top7 日总榜;
  4. 同步刷新rank:hot:white_list头部爆款白名单,更新免门槛商品范围;
  5. 不改动当日 curr 榜单,当日榜单依靠日间订单实时填充。

步骤 7:每 2 小时轻巡检,防止榜单长期僵死

巡检范围仅两个小集合:头部白名单 + 15 天新品池,总量上限 3000 条:

  1. 批量从 MySQL 拉取精准 7 日分数、当日销量分数;
  2. 批量覆盖更新两个榜单 ZSet;
  3. 即便全天极少商品达标销量阈值,榜单每两小时自动校准一次,不会一整天静止不动。

六、冷启动完整解决方案

1. 单个新品 SPU 冷启动

  1. 独立新品池表,扶持周期 15 天,配置阶梯衰减基础分(1-5 天高值、11-15 天低值);
  2. 新品完全不受 20 单阈值限制,成交 1 单即可计算带基础分的总分、冲榜;
  3. 15 天后基础分清零,转为普通商品走阈值规则。

2. Redis 整体宕机 / 清空冷启动

  1. 7 日总榜:直接读取 goods_rank_total 预计算分数,LIMIT 2000 批量 ZADD 重建,毫秒级恢复;
  2. 当日榜单:读取 order_day_spu 今日动销数据重建 curr,无重建间隙时先用 last 昨日榜单兜底页面;
  3. MySQL 汇总表永久留存真值,Redis 只是缓存层,不存在不可逆数据丢失。

3. 平台全新从零搭建冷启动

  1. 回放近 7 天订单消息,批量生成 hour/day 两层汇总表;
  2. 一次性计算全部动销商品分数,初始化 7 日总榜、白名单;
  3. 初始化 curr/last 双当日榜单,切换至正常日间阈值流程。

七、性能、容错、大促降级机制

1. 数据库压力压制

95% 冷门商品无任何聚合查询;计算池永远只有动销 SPU,每日计算量削减 90%+;统计查询全部路由 MySQL 从库。

2. Redis 数据漂移防护

小时级 DB 校对实时销量计数、凌晨全局全覆盖重算,双层抵消加减偏差、消息丢包误差。

3. 退款、刷单数据防失真

支付正向 INCR、退款反向 DECR;全局订单幂等 Set 拦截重复消息;风控过滤虚假订单,脏数据不进入计数链路。

4. 大促峰值降级开关

  • 抬高当日榜单阈值(20→50)
  • 临时关闭浏览、收藏行为权重,仅以销量计分
  • 缩减 TOP 缓存容量(2000→1000)
  • 可临时关闭 2 小时巡检,只保留凌晨一次校准

八、分页查询逻辑(用户无感知分层路由)

  1. 7 日综合榜
  • 排名前 2000:Redis ZREVRANGE 毫秒返回
  • 2000 名之后冷门:自动切换 MySQL goods_rank_total 分页查询

2.当日销量榜

  • 优先 curr 实时榜单;数量不足自动切 last 昨日榜单兜底
  • 分页深度过大同样兜底走 MySQL 基准表

九、方案横向对比表

|-----------------|--------|-----------|-------|--------|--------------|--------------|
| 方案 | 数据库压力 | Redis 压力 | 实时性 | 精准度 | 落地难度 | 页面空白风险 |
| 全量 MySQL 定时计算 | 极高 | 低 | 差 | 高 | 简单,性能差 | 低 |
| 全量商品塞入 ZSet | 低 | 极高 (阻塞风险) | 高 | 高 | 简单,不稳定 | 低 |
| 小时分片重型架构 | 极低 | 中 | 极高 | 极高 | 极重 (集群基建) | 低 |
| 本文阈值准入双榜单方案 | 极低 | 极低 | | 极高 | 极简中小团队友好 | 零空白,自动兜底 |

十、最终总结

  1. 核心思路「销量阈值准入 ZSet」完美解决冷热资源分配问题,摒弃无效全量计算;
  2. 7 日综合榜单常驻不删除,天然保障首页流量兜底;
  3. 当日销量榜单采用 curr/last 双 key 轮换,接口层自动识别数据多少,不足时复用昨日榜单,彻底解决零点新开页面空白;
  4. 白名单爆款、新品豁免门槛、凌晨动态降阈值、两小时巡检多层兜底,不会出现榜单一整天静止不动;
  5. 整套体系以 MySQL 汇总数据为唯一真值,Redis 只做加速缓存,容错、扩容、大促降级全部配套完善,十万、百万 SPU 体量均可稳定长期运行,是垂直电商、自营商城、创业团队性价比最高的排行榜落地方案。