在电商平台的日常运营中,实时热销商品排行榜是提升用户决策效率、拉动高转化商品曝光的核心功能。当数据量达到千万级时,传统数据库的排序查询会面临性能瓶颈,而 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.1    port: 6379    password: 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;  // 商品ID    private 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;    // 构造注入(避免字段注入)    @Autowired    public 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;    @Autowired    public 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    @AllArgsConstructor    public 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;    @Autowired    public 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;    @Autowired    public HotProductRankApplication(TestDataGenerator testDataGenerator) {        this.testDataGenerator = testDataGenerator;    }    public static void main(String[] args) {        SpringApplication.run(HotProductRankApplication.class, args);    }    // 启动后执行测试数据生成    @Override    public 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

生产环境注意事项

  1. 测试数据控制

    TestDataGenerator 仅在 dev 环境启用,生产环境需通过 spring.profiles.active=prod 禁用,避免误生成大量测试数据;

  2. 性能监控

    部署 Prometheus + Grafana,监控 Redis 的 used_memory(内存使用)、zadd/zrevrange 命令耗时、SpringBoot 接口响应时间,阈值告警(如接口耗时超过 50ms 告警);

  3. 避免大 Key

    单排行榜 Key 的数据量建议不超过 1000 万,若商品分类过多,可按 “分类 + 日期” 拆分 Key(如 hot_products:home_appliance:20241001),定期清理历史 Key;

  4. 数据同步

    若商品分数来源于 MySQL(如销量、好评数),通过 Canal 监听 MySQL binlog,实时同步数据到 Redis,避免人工调用接口更新;

  5. Redis 集群部署

    千万级数据需至少 3 主 3 从 Redis Cluster,每个主节点内存配置不低于 8GB,确保分片均匀和高可用。

7

系统优化建议

  • 引入 Redisearch 插件,实现 “按商品名模糊搜索并返回排名”(如搜索 “冰箱”,返回冰箱类商品的排行榜);

  • 对 Top 100 商品的详情信息(如名称、图片、价格)缓存到本地 Caffeine,减少 Redis 访问次数;

  • 通过 Redis 的 Keyspace Notifications(键空间通知),监听排行榜分数变动,实时推送排名变化到前端(如 WebSocket),提升用户体验。

通过以上设计,系统可稳定支持千万级数据量的实时排行榜需求,兼顾性能、一致性和可扩展性,完全满足电商平台的生产级应用场景。

8

后端技术交流群

我们致力于创建一个高质量的技术交流社区,欢迎编程开发者和技术招聘HR专业人士加入。同时,我们也鼓励大家分享自己公司的内部推荐机会,互相协作,共同提升!

交流技术 职位内推 行业探讨

广告人士勿入,切勿轻信私聊,防止被骗

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐