SpringBoot + Redis 实战:千万级数据实时热销商品排行榜系统搭建
在电商平台的日常运营中,实时热销商品排行榜是提升用户决策效率、拉动高转化商品曝光的核心功能。当数据量达到千万级时,传统数据库的排序查询会面临性能瓶颈,而 Redis 的 Sorted Set(有序集合)凭借其高效的插入、排序和查询特性,成为实现该功能的最优解。本文将详细讲解如何基于 SpringBoot 整合 Redis,搭建支持千万级数据量的实时热销商品排行榜系统。

1
核心客户群体与价值
排行榜系统需满足多角色的核心需求,其设计需围绕业务价值展开:
-
运营人员:不仅需要实时监控热销商品的排名波动趋势,还能基于排行榜数据制定定向促销策略(如对 Top 10 商品推出 “组合优惠”),同时识别 “潜力商品”(排名快速上升的商品)进行资源倾斜。
-
商家:通过查看自家商品的排名和评分(如销量、好评率加权分),可精准调整库存备货策略(避免 Top 商品缺货),优化供应链节奏,同时对标竞品排名优化商品详情页和定价。
-
消费者:无需在海量商品中筛选,通过排行榜快速定位 “高人气、高口碑” 商品,缩短决策路径,同时降低 “踩坑” 风险(避免购买冷门低质商品)。
2
核心功能需求定义
基于业务场景,系统需实现 5 个核心功能,且每个功能需满足 “实时性、高可用” 要求:
|
功能模块 |
详细描述 |
输入参数 |
输出结果 |
|---|---|---|---|
|
商品添加 / 分数更新 |
支持新商品加入排行榜,或已有商品分数累加(如销量每增加 1,分数 +1) |
productId(商品 ID)、score(分数)、category(分类,可选) |
操作结果(成功 / 失败)、时间戳 |
|
商品从排行榜移除 |
支持因商品下架、违规等场景移除商品,采用硬删除(避免无效数据占用内存) |
productId、category(可选) |
操作结果(成功 / 失败)、删除条数 |
|
查询商品实时排名 |
按分数降序排列(分数越高排名越前),返回排名(从 0 开始,前端需 +1 展示) |
productId、category(可选) |
商品排名(无此商品返回 -1) |
|
查询商品当前评分 |
实时返回商品的累计分数(如分数 = 销量 ×0.6 + 好评数 ×0.4) |
productId、category(可选) |
商品分数(无此商品返回 null) |
|
获取 Top N 热销商品 |
支持按分类筛选,返回前 N 个商品的 ID 和分数(方便前端展示 “销量 / 热度”) |
count(数量)、category(可选) |
商品列表(含 productId、score) |
3
系统设计关键技术点
千万级数据场景下,系统需突破 “性能、一致性、可扩展” 三大核心挑战,关键设计如下:
1. 高性能与低延迟
-
利用 Redis 单线程模型(避免线程切换开销)处理并发请求,配合 SpringBoot 的异步接口(@Async)减少请求阻塞;
-
配置 Redis 连接池(如 lettuce 连接池),设置 max-active=200、max-idle=50、min-idle=20,避免频繁创建 / 销毁连接;
-
基于 Redis Sorted Set 的 O (logN) 插入 / 更新效率,确保商品分数变动实时同步到排行榜。
2. 数据一致性保障
-
使用 Redis 原生命令(如 ZADD、ZINCRBY)保证单操作原子性;复杂场景(如 “更新分数 + 记录日志”)通过 Lua 脚本封装,避免分布式事务问题;
-
若采用多 Redis 节点,通过 Redis Cluster 的哈希槽分片(16384 个槽)确保数据均匀分布,同时开启副本同步(replicaof)避免单点数据丢失。
3. 可扩展性设计
-
前端通过 Nginx 转发请求到多 SpringBoot 实例,后端 Redis Cluster 自动实现节点负载分担;
-
按商品分类(如 “家电”“服装”)拆分排行榜 Key(如 hotproducts:homeappliance、hot_products:clothing),避免单 Key 数据量过大(单 Key 建议不超过 1000 万)。
4. 持久化与容灾
-
开启 Redis 的 RDB(定时全量备份)+ AOF(实时增量日志)混合模式,RDB 用于快速恢复,AOF 确保数据丢失不超过 1 秒;
-
Redis Cluster 支持自动故障转移(当主节点下线,从节点通过选举成为新主),恢复时间控制在 10 秒内。
5. 内存与安全优化
-
设置 Redis 最大内存(maxmemory),搭配 volatile-lru 淘汰策略(优先淘汰过期的排行榜数据);定期清理历史数据(如超过 30 天的冷门商品);
-
配置 Redis 密码(requirepass)、IP 白名单(bind 内网 IP),禁用危险命令(如 FLUSHDB、KEYS),传输层启用 TLS 加密(避免数据泄露)。
4
代码实操:从 0 到 1 搭建系统
1. 依赖引入(pom.xml)
核心依赖包括 SpringBoot Web、Spring Data Redis、Lettuce 连接池(默认)、Lombok,确保版本兼容(SpringBoot 2.7.x + Redis 6.x):
<dependencies><!-- SpringBoot Web 核心 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Data Redis(集成 Lettuce 连接池) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Lombok 简化代码 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- 测试依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
2. Redis 配置(application.yml)
相比原文的极简配置,此处补充连接池、持久化、超时时间等生产级配置:
spring:redis:# 基础连接信息host: 127.0.0.1port: 6379password: your_redis_password # 生产环境必须设置密码database: 0 # 选择第 0 个数据库(避免与其他业务冲突)# 连接池配置(Lettuce)lettuce:pool:max-active: 200 # 最大活跃连接数max-idle: 50 # 最大空闲连接数min-idle: 20 # 最小空闲连接数max-wait: 3000ms # 连接获取超时时间# 超时配置timeout: 5000ms # 命令执行超时时间# 多环境区分(测试环境才生成测试数据)profiles:active: dev
3. 核心实体与 DTO
-
补充分类字段,支持多分类排行榜;
-
返回前端时包含商品 ID、分数、排名,避免前端二次计算:
// 商品实体类@Data@NoArgsConstructor@AllArgsConstructorpublic class Product {private String id; // 商品ID(如 product_123)private String name; // 商品名称private String category; // 商品分类(如 home_appliance)}// 排行榜返回DTO@Data@NoArgsConstructor@AllArgsConstructorpublic class ProductRankDTO {private String productId; // 商品IDprivate Double score; // 商品分数private Long rank; // 商品排名(从 1 开始)}
4. Redis 核心服务类
封装排行榜的所有操作,新增分数累加、按分类查询功能,优化返回结果:
@Service@Slf4jpublic class HotProductService {// 排行榜Key前缀(拼接分类形成最终Key,如 hot_products:home_appliance)private static final String RANK_KEY_PREFIX = "hot_products:";private final StringRedisTemplate redisTemplate;// 构造注入(避免字段注入)@Autowiredpublic HotProductService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}/*** 1. 添加/更新商品分数(支持分数累加)* @param productId 商品ID* @param score 分数(正数累加,负数减少)* @param category 商品分类(默认 default)* @return 操作后的分数*/public Double addOrUpdateProductScore(String productId, double score, String category) {if (StringUtils.isEmpty(productId) || score == 0) {throw new IllegalArgumentException("商品ID不能为空,分数不能为0");}String rankKey = getRankKey(category);try {// ZINCRBY:原子性累加分数,不存在则创建return redisTemplate.opsForZSet().incrementScore(rankKey, productId, score);} catch (Exception e) {log.error("更新商品[{}]分数失败", productId, e);throw new RuntimeException("排行榜操作失败");}}/*** 2. 从排行榜移除商品* @param productId 商品ID* @param category 商品分类* @return 移除的条数(0表示无此商品)*/public Long removeProductFromRank(String productId, String category) {if (StringUtils.isEmpty(productId)) {throw new IllegalArgumentException("商品ID不能为空");}String rankKey = getRankKey(category);try {return redisTemplate.opsForZSet().remove(rankKey, productId);} catch (Exception e) {log.error("移除商品[{}]失败", productId, e);throw new RuntimeException("排行榜操作失败");}}/*** 3. 查询商品排名(降序,分数越高排名越前)* @param productId 商品ID* @param category 商品分类* @return 排名(从 1 开始,无此商品返回 -1)*/public Long getProductRank(String productId, String category) {if (StringUtils.isEmpty(productId)) {throw new IllegalArgumentException("商品ID不能为空");}String rankKey = getRankKey(category);// ZREVRANK:返回降序排名(0开始),转成1开始的排名Long rank = redisTemplate.opsForZSet().reverseRank(rankKey, productId);return rank == null ? -1 : rank + 1;}/*** 4. 查询商品当前分数* @param productId 商品ID* @param category 商品分类* @return 分数(无此商品返回 null)*/public Double getProductScore(String productId, String category) {if (StringUtils.isEmpty(productId)) {throw new IllegalArgumentException("商品ID不能为空");}String rankKey = getRankKey(category);return redisTemplate.opsForZSet().score(rankKey, productId);}/*** 5. 获取 Top N 商品(带排名和分数)* @param count 数量(如 10 表示 Top 10)* @param category 商品分类* @return 商品排名列表(按排名升序)*/public List<ProductRankDTO> getTopNProducts(int count, String category) {if (count <= 0 || count > 1000) {throw new IllegalArgumentException("数量需在 1-1000 之间");}String rankKey = getRankKey(category);// ZREVRANGE_WITHSCORES:降序获取前 N 条(0 到 count-1),包含分数Set<ZSetOperations.TypedTuple<String>> tupleSet =redisTemplate.opsForZSet().reverseRangeWithScores(rankKey, 0, count - 1);if (CollectionUtils.isEmpty(tupleSet)) {return Collections.emptyList();}// 转换为 DTO 列表(排名从 1 开始)List<ProductRankDTO> rankList = new ArrayList<>();long rank = 1;for (ZSetOperations.TypedTuple<String> tuple : tupleSet) {rankList.add(new ProductRankDTO(tuple.getValue(),tuple.getScore(),rank++));}return rankList;}/*** 生成排行榜Key(前缀 + 分类,分类为空则用 default)*/private String getRankKey(String category) {return RANK_KEY_PREFIX + (StringUtils.isEmpty(category) ? "default" : category);}}
5. REST 接口控制器
提供 RESTful 接口,统一响应格式,支持前端调用:
@RestController@RequestMapping("/api/hot-products")@Slf4jpublic class HotProductController {private final HotProductService hotProductService;@Autowiredpublic HotProductController(HotProductService hotProductService) {this.hotProductService = hotProductService;}// 1. 添加/更新商品分数@PostMapping("/score")public ResponseEntity<ResultDTO> updateProductScore(@RequestParam String productId,@RequestParam double score,@RequestParam(required = false, defaultValue = "default") String category) {Double newScore = hotProductService.addOrUpdateProductScore(productId, score, category);return ResponseEntity.ok(ResultDTO.success("分数更新成功", newScore));}// 2. 移除商品@DeleteMapping("/{productId}")public ResponseEntity<ResultDTO> removeProduct(@PathVariable String productId,@RequestParam(required = false, defaultValue = "default") String category) {Long removeCount = hotProductService.removeProductFromRank(productId, category);String msg = removeCount > 0 ? "商品移除成功" : "商品不存在";return ResponseEntity.ok(ResultDTO.success(msg, removeCount));}// 3. 查询商品排名@GetMapping("/{productId}/rank")public ResponseEntity<ResultDTO> getProductRank(@PathVariable String productId,@RequestParam(required = false, defaultValue = "default") String category) {Long rank = hotProductService.getProductRank(productId, category);return ResponseEntity.ok(ResultDTO.success("排名查询成功", rank));}// 4. 查询商品分数@GetMapping("/{productId}/score")public ResponseEntity<ResultDTO> getProductScore(@PathVariable String productId,@RequestParam(required = false, defaultValue = "default") String category) {Double score = hotProductService.getProductScore(productId, category);return ResponseEntity.ok(ResultDTO.success("分数查询成功", score));}// 5. 获取 Top N 商品@GetMapping("/top/{count}")public ResponseEntity<ResultDTO> getTopNProducts(@PathVariable int count,@RequestParam(required = false, defaultValue = "default") String category) {List<ProductRankDTO> topList = hotProductService.getTopNProducts(count, category);return ResponseEntity.ok(ResultDTO.success("Top N 查询成功", topList));}// 统一响应DTO@Data@NoArgsConstructor@AllArgsConstructorpublic static class ResultDTO {private int code; // 状态码(200成功,500失败)private String msg; // 消息private Object data; // 数据public static ResultDTO success(String msg, Object data) {return new ResultDTO(200, msg, data);}public static ResultDTO fail(String msg) {return new ResultDTO(500, msg, null);}}}
6. 测试数据生成器(仅 dev 环境)
使用 Redis Pipeline 批量插入数据,提升千万级数据的插入效率(避免单条插入的网络开销):
@Component@Profile("dev") // 仅在 dev 环境生效@Slf4jpublic class TestDataGenerator {private final HotProductService hotProductService;private final StringRedisTemplate redisTemplate;@Autowiredpublic TestDataGenerator(HotProductService hotProductService, StringRedisTemplate redisTemplate) {this.hotProductService = hotProductService;this.redisTemplate = redisTemplate;}/*** 生成千万级测试数据(按分类拆分,避免单Key过大)*/@Async // 异步执行,不阻塞启动public void generateMillionTestData() {log.info("开始生成千万级测试数据...");long start = System.currentTimeMillis();// 按 3 个分类拆分数据(每个分类约 333 万条)String[] categories = {"home_appliance", "clothing", "electronics"};for (String category : categories) {String rankKey = "hot_products:" + category;// 使用 Pipeline 批量插入(减少网络往返)redisTemplate.executePipelined((RedisCallback<Void>) connection -> {for (int i = 1; i <= 3333333; i++) {String productId = "product_" + category + "_" + i;double score = Math.random() * 100000; // 分数 0-100000// 批量执行 ZADD 命令connection.zAdd(rankKey.getBytes(StandardCharsets.UTF_8),score,productId.getBytes(StandardCharsets.UTF_8));}return null;});log.info("分类[{}]数据生成完成,约 333 万条", category);}long end = System.currentTimeMillis();log.info("千万级测试数据生成完成,耗时:{} 秒", (end - start) / 1000);}}
7. 启动类
实现 CommandLineRunner,在启动后自动执行测试数据生成(仅 dev 环境):
@SpringBootApplication@EnableAsync // 开启异步支持public class HotProductRankApplication implements CommandLineRunner {private final TestDataGenerator testDataGenerator;@Autowiredpublic HotProductRankApplication(TestDataGenerator testDataGenerator) {this.testDataGenerator = testDataGenerator;}public static void main(String[] args) {SpringApplication.run(HotProductRankApplication.class, args);}// 启动后执行测试数据生成@Overridepublic void run(String... args) throws Exception {testDataGenerator.generateMillionTestData();}}
5
测试结果验证
启动应用后(dev 环境),通过 Postman 或 curl 调用接口,验证功能正确性:
1. 获取家电分类 Top 5 商品
GET /api/hot-products/top/5?category=home_appliance
响应:
{"code": 200,"msg": "Top N 查询成功","data": [{"productId": "product_home_appliance_123456","score": 99876.54,"rank": 1},{"productId": "product_home_appliance_789012","score": 99765.32,"rank": 2},{"productId": "product_home_appliance_345678","score": 99654.10,"rank": 3},{"productId": "product_home_appliance_901234","score": 99543.88,"rank": 4},{"productId": "product_home_appliance_567890","score": 99432.66,"rank": 5}]}
2. 查询指定商品排名
-
请求:
GET /api/hot-products/product_home_appliance_123456/rank?category=home_appliance -
响应:
{"code": 200,"msg": "排名查询成功","data": 1}
3. 累加商品分数(如销量增加 100,分数 +100)
-
请求:
POST /api/hot-products/score?productId=product_home_appliance_123456&score=100&category=home_appliance -
响应:
{"code": 200,"msg": "分数更新成功","data": 99976.54}
6
生产环境注意事项
- 测试数据控制
TestDataGenerator仅在 dev 环境启用,生产环境需通过spring.profiles.active=prod禁用,避免误生成大量测试数据; - 性能监控
部署 Prometheus + Grafana,监控 Redis 的
used_memory(内存使用)、zadd/zrevrange命令耗时、SpringBoot 接口响应时间,阈值告警(如接口耗时超过 50ms 告警); - 避免大 Key
单排行榜 Key 的数据量建议不超过 1000 万,若商品分类过多,可按 “分类 + 日期” 拆分 Key(如 hot_products:home_appliance:20241001),定期清理历史 Key;
- 数据同步
若商品分数来源于 MySQL(如销量、好评数),通过 Canal 监听 MySQL binlog,实时同步数据到 Redis,避免人工调用接口更新;
- Redis 集群部署
千万级数据需至少 3 主 3 从 Redis Cluster,每个主节点内存配置不低于 8GB,确保分片均匀和高可用。
7
系统优化建议
-
引入 Redisearch 插件,实现 “按商品名模糊搜索并返回排名”(如搜索 “冰箱”,返回冰箱类商品的排行榜);
-
对 Top 100 商品的详情信息(如名称、图片、价格)缓存到本地 Caffeine,减少 Redis 访问次数;
-
通过 Redis 的 Keyspace Notifications(键空间通知),监听排行榜分数变动,实时推送排名变化到前端(如 WebSocket),提升用户体验。
通过以上设计,系统可稳定支持千万级数据量的实时排行榜需求,兼顾性能、一致性和可扩展性,完全满足电商平台的生产级应用场景。
8
后端技术交流群
我们致力于创建一个高质量的技术交流社区,欢迎编程开发者和技术招聘HR专业人士加入。同时,我们也鼓励大家分享自己公司的内部推荐机会,互相协作,共同提升!
交流技术 职位内推 行业探讨
广告人士勿入,切勿轻信私聊,防止被骗
更多推荐
所有评论(0)