SpringBoot开发双11商品服务系统 | 已完结

55 阅读9分钟

370c29f661e0d8f6a0c8ebaa7cd40de.png

SpringBoot 实战:打造高可用双 11 商品服务系统,应对千万级并发

双 11 作为电商行业的年度峰值场景,商品服务作为核心流量入口,需承载 “千万级 QPS 查询”“瞬时秒杀抢购”“海量商品数据同步” 等极端需求。基于 SpringBoot 构建商品服务,可借助其 “自动配置、快速开发、生态丰富” 的优势,结合 Redis、Elasticsearch、RabbitMQ 等中间件,打造高可用、高并发、可扩展的服务架构。本文将从 “需求分析 — 架构设计 — 核心功能实现 — 高并发优化 — 部署运维” 全流程,带您落地双 11 级别的商品服务系统。

一、先明确:双 11 商品服务的核心需求与技术挑战

在动手开发前,需先梳理双 11 场景下商品服务的特殊需求,这是架构设计与技术选型的基础。

1. 核心业务需求

双 11 商品服务需支撑 “浏览 — 加购 — 抢购 — 下单” 全链路,核心功能包括:

  • 商品基础信息管理:支持千万级 SKU(最小库存单位)的新增、编辑、上下架,包含商品名称、价格、库存、规格(颜色 / 尺寸)、详情页等信息;
  • 商品搜索与推荐:用户输入关键词(如 “羽绒服”),需在 100ms 内返回精准结果,支持 “销量排序”“价格筛选”“品牌过滤” 等功能;
  • 库存管理:双 11 秒杀场景下需 “防超卖”,支持 “预扣库存 — 支付减库存 — 超时回滚” 的完整流程,同时应对 “分布式库存一致性” 问题;
  • 价格管控:支持 “双 11 预热价 — 峰值价 — 返场价” 的动态调价,且价格修改需 “实时生效、不可篡改”,避免用户投诉;
  • 高并发查询:商品列表页、详情页在双 11 峰值(如 0 点抢购)需承载千万级 QPS,页面响应时间需控制在 200ms 内。

2. 关键技术挑战

双 11 场景的极端流量对服务提出三大挑战:

  • 高并发查询压力:商品详情页作为流量入口,双 11 0 点 QPS 可能突破 10 万 / 秒,传统 “数据库直接查询” 会导致连接耗尽、查询超时;
  • 瞬时秒杀超卖:热门商品秒杀时,数万用户同时抢购,若库存扣减逻辑设计不当,易出现 “超卖”(实际库存为 0 却下单成功)或 “少卖”(库存未售完却无法下单);
  • 数据一致性:商品信息(价格、库存)需在 “服务集群 — 缓存 — 数据库” 间保持一致,避免 “缓存显示有库存,实际已售罄” 的用户体验问题。

二、架构设计:SpringBoot + 中间件构建高可用架构

针对双 11 需求与挑战,采用 “分层架构 + 微服务拆分 + 中间件协同” 的设计思路,确保服务的高可用与可扩展性。

1. 整体架构分层

基于 SpringBoot 构建的商品服务,从下至上分为 “数据层 — 缓存层 — 服务层 — 网关层”,每层职责明确且解耦:

  • 数据层:负责商品数据的持久化存储,采用 “MySQL(结构化数据)+ Elasticsearch(搜索数据)” 双存储方案;
    • MySQL:存储商品基础信息(ID、名称、价格、库存、规格)、库存明细等结构化数据,使用分库分表(Sharding-JDBC)应对千万级 SKU 存储;
    • Elasticsearch:存储商品搜索数据(关键词、分类、标签),支持全文检索与复杂筛选;
  • 缓存层:采用 Redis 集群实现 “热点数据缓存”,缓解数据库查询压力,分为三级缓存:
    • 本地缓存(Caffeine):缓存高频访问的热门商品(如 TOP1000 商品),响应时间 < 1ms;
    • Redis 分布式缓存:缓存全量商品详情、库存计数,支持分布式锁与原子操作;
    • 浏览器缓存:通过 HTTP 缓存头(Cache-Control、ETag)缓存商品列表页静态资源(图片、CSS);
  • 服务层:基于 SpringBoot+SpringCloud Alibaba 实现微服务拆分,核心服务包括:
    • 商品基础服务(product-base):处理商品增删改查、上下架;
    • 商品搜索服务(product-search):对接 Elasticsearch,提供搜索与筛选;
    • 库存服务(product-inventory):负责库存预扣、扣减、回滚;
    • 价格服务(product-price):管理动态调价、价格校验;
  • 网关层:使用 Spring Cloud Gateway 作为入口,实现 “路由转发、限流熔断、灰度发布”,拦截无效请求,保护后端服务。

2. 核心技术选型

技术组件选型核心作用双 11 场景价值
开发框架SpringBoot 2.7.x快速开发、自动配置、简化依赖管理减少 70% 配置代码,加速迭代效率
微服务框架SpringCloud Alibaba服务注册发现(Nacos)、配置中心(Nacos)支持服务动态扩容、配置实时更新
数据存储MySQL 8.0 + Sharding-JDBC结构化数据存储、分库分表支撑千万级 SKU 存储,避免单库性能瓶颈
搜索引擎Elasticsearch 7.17全文检索、复杂筛选100ms 内返回搜索结果,支持双 11 大促筛选
缓存Redis 6.x 集群分布式缓存、分布式锁、原子操作承载 90% 商品查询流量,避免数据库压垮
消息队列RabbitMQ 3.11异步通信、流量削峰、数据同步削峰秒杀流量,同步商品数据到 ES/Redis
限流熔断Sentinel 1.8流量控制、熔断降级、系统保护防止双 11 峰值流量压垮服务,保障可用性
分布式事务Seata AT 模式解决分布式场景下的数据一致性确保 “库存扣减 — 订单创建” 事务一致性

三、核心功能实现:SpringBoot 落地双 11 关键场景

基于上述架构,聚焦双 11 商品服务的三大核心场景,详解 SpringBoot 的实战实现。

场景 1:商品详情页高并发查询(支撑 10 万 QPS)

商品详情页是双 11 流量最高的页面,需通过 “多级缓存 + 异步加载” 优化查询性能,核心实现步骤如下:

1. 三级缓存协同查询

采用 “本地缓存(Caffeine)→ Redis → MySQL” 的查询链路,优先从缓存获取数据,减少数据库访问:

  • Step 1:本地缓存查询:使用 Caffeine 缓存热门商品(如近 1 小时访问量 TOP1000 商品),配置 “最大容量 10000、过期时间 5 分钟”,代码示例:
@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<Long, ProductDetailDTO> productLocalCache() {
        return Caffeine.newBuilder()
                .maximumSize(10000) // 最大缓存条目
                .expireAfterWrite(5, TimeUnit.MINUTES) // 写入后5分钟过期
                .build();
    }
}
@Service
public class ProductDetailService {
    @Autowired
    private Cache<Long, ProductDetailDTO> productLocalCache;
    @Autowired
    private RedisTemplate<String, ProductDetailDTO> redisTemplate;
    @Autowired
    private ProductMapper productMapper;
    // 查询商品详情
    public ProductDetailDTO getProductDetail(Long productId) {
        // 1. 查本地缓存
        ProductDetailDTO detailDTO = productLocalCache.getIfPresent(productId);
        if (detailDTO != null) {
            log.info("本地缓存命中,商品ID:{}", productId);
            return detailDTO;
        }
        // 2. 查Redis缓存
        String redisKey = "product:detail:" + productId;
        detailDTO = redisTemplate.opsForValue().get(redisKey);
        if (detailDTO != null) {
            log.info("Redis缓存命中,商品ID:{}", productId);
            // 回写本地缓存,提升下次查询效率
            productLocalCache.put(productId, detailDTO);
            return detailDTO;
        }
        // 3. 查MySQL数据库
        detailDTO = productMapper.selectDetailById(productId);
        if (detailDTO == null) {
            throw new BusinessException(404, "商品不存在");
        }
        // 4. 缓存预热:写入Redis(过期时间1小时)和本地缓存
        redisTemplate.opsForValue().set(redisKey, detailDTO, 1, TimeUnit.HOURS);
        productLocalCache.put(productId, detailDTO);
        log.info("数据库查询命中,商品ID:{},已写入缓存", productId);
        return detailDTO;
    }
}
  • Step 2:缓存预热:双 11 前 1 小时,通过定时任务(Spring Schedule)将热门商品(如预售 TOP1000)提前加载到 Redis 与本地缓存,避免 “缓存穿透”(大量请求直达数据库):
@Scheduled(cron = "0 0 23 * * ?") // 双111小时(23点)执行
public void preloadHotProductCache() {
    log.info("开始预热热门商品缓存");
    // 查询预售TOP1000商品ID
    List<Long> hotProductIds = productMapper.selectHotProductTop1000();
    for (Long productId : hotProductIds) {
        ProductDetailDTO detailDTO = productMapper.selectDetailById(productId);
        // 写入Redis
        String redisKey = "product:detail:" + productId;
        redisTemplate.opsForValue().set(redisKey, detailDTO, 2, TimeUnit.HOURS);
        // 写入本地缓存
        productLocalCache.put(productId, detailDTO);
    }
    log.info("热门商品缓存预热完成,共加载{}个商品", hotProductIds.size());
}
2. 异步加载非核心数据

商品详情页中的 “用户评价”“推荐商品” 等非核心数据,采用 “异步加载” 方式,先返回核心信息(名称、价格、库存),再通过 AJAX 异步获取非核心数据,减少首屏加载时间:

  • Controller 层实现
@RestController
@RequestMapping("/api/product")
public class ProductController {
    @Autowired
    private ProductDetailService detailService;
    @Autowired
    private AsyncProductService asyncProductService;
    // 核心接口:返回商品核心信息(首屏加载)
    @GetMapping("/detail/{productId}")
    public Result<ProductCoreDTO> getProductCore(@PathVariable Long productId) {
        ProductDetailDTO detailDTO = detailService.getProductDetail(productId);
        // 提取核心信息(名称、价格、库存、主图)
        ProductCoreDTO coreDTO = ProductCoreDTO.builder()
                .productId(detailDTO.getProductId())
                .name(detailDTO.getName())
                .price(detailDTO.getPrice())
                .stock(detailDTO.getStock())
                .mainImage(detailDTO.getMainImage())
                .build();
        return Result.success(coreDTO);
    }
    // 异步接口:返回非核心信息(评价、推荐商品)
    @GetMapping("/detail/extra/{productId}")
    public CompletableFuture<Result<ProductExtraDTO>> getProductExtra(@PathVariable Long productId) {
        // 使用Spring Async异步执行,避免阻塞主线程
        return asyncProductService.getProductExtra(productId)
                .thenApply(Result::success);
    }
}
// 异步服务层(@Async需配合@EnableAsync启用)
@Service
@EnableAsync
public class AsyncProductService {
    @Autowired
    private CommentMapper commentMapper;
    @Autowired
    private RecommendService recommendService;
    @Async
    public CompletableFuture<ProductExtraDTO> getProductExtra(Long productId) {
        // 异步查询评价(前10条)
        List<CommentDTO> comments = commentMapper.selectTop10ByProductId(productId);
        // 异步查询推荐商品(相似商品)
        List<ProductSimpleDTO> recommends = recommendService.getSimilarProducts(productId);
        // 组装返回结果
        ProductExtraDTO extraDTO = ProductExtraDTO.builder()
                .comments(comments)
                .recommends(recommends)
                .build();
        return CompletableFuture.completedFuture(extraDTO);
    }
}

场景 2:秒杀库存防超卖(支撑万级并发抢购)

双 11 秒杀场景下,库存扣减是核心痛点,需通过 “Redis 预扣 + 数据库最终一致性 + 分布式锁” 实现防超卖,核心步骤如下:

1. 库存预热:Redis 初始化库存

秒杀活动开始前,将商品库存从 MySQL 同步到 Redis,用 “Redis 原子计数器” 记录可售库存,代码示例:

@Service
public class SeckillInventoryService {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    @Autowired
    private InventoryMapper inventoryMapper;
    // 秒杀前预热库存到Redis
    public void preloadSeckillInventory(Long seckillId, Long productId) {
        // 1. 从MySQL查询秒杀库存(单独存储秒杀库存,与普通库存隔离)
        SeckillInventoryDO inventoryDO = inventoryMapper.selectBySeckillId(seckillId);
        if (inventoryDO == null || inventoryDO.getSeckillStock() <= 0) {
            throw new BusinessException(500, "秒杀库存不足");
        }
        // 2. 写入Redis:key=seckill:stock:{seckillId}, value=可售库存
        String redisStockKey = "seckill:stock:" + seckillId;
        redisTemplate.opsForValue().set(redisStockKey, inventoryDO.getSeckillStock());
        // 3. 初始化库存预热标记(避免重复预热)
        String preloadFlagKey = "seckill:preload:flag:" + seckillId;
        redisTemplate.opsForValue().set(preloadFlagKey, "1", 24, TimeUnit.HOURS);
        log.info("秒杀库存预热完成,秒杀ID:{},商品ID:{},库存:{}", 
                 seckillId, productId, inventoryDO.getSeckillStock());
    }
}
2. 库存预扣:Redis 原子操作防超卖

用户抢购时,先通过 Redis 的decr原子操作预扣库存,若预扣成功则生成订单,失败则直接返回 “已抢完”,避免超卖:

@Service
public class SeckillService {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    // 秒杀抢购核心方法
    public Result<String> doSeckill(Long seckillId, Long productId, Long userId) {
        // 1. 验证用户是否已抢购(避免重复下单,Redis记录已抢购用户)
        String userSeckillKey = "seckill:user:" + seckillId;
        Boolean isMember = redisTemplate.opsForSet().isMember(userSeckillKey, userId);
        if (Boolean.TRUE.equals(isMember)) {
            return Result.fail("您已参与过该商品秒杀,不可重复抢购");
        }
        // 2. Redis原子预扣库存(decr操作,返回扣减后的值)
        String redisStockKey = "seckill:stock:" + seckillId;
        Integer remainStock = redisTemplate.opsForValue().decrement(redisStockKey);
        if (remainStock == null || remainStock < 0) {
            // 库存不足,回补decr(避免负数占用)
            redisTemplate.opsForValue().increment(redisStockKey);
            return Result.fail("手慢了!商品已抢完");
        }
        // 3. 记录用户抢购记录(Redis Set,避免重复下单)
        redisTemplate.opsForSet().add(userSeckillKey, userId);
        // 4. 发送消息到RabbitMQ,异步创建订单、扣减MySQL库存
        SeckillMessage message = SeckillMessage.builder()
                .seckillId(seckillId)
                .productId(productId)
                .userId(userId)
                .build();
        rabbitTemplate.convertAndSend("seckill-exchange", "seckill.routing.key", message);
        // 5. 返回抢购成功(订单创建异步完成,后续通过轮询或WebSocket通知用户)
        return Result.success("抢购成功,订单正在创建中");
    }
}
3