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 * * ?") // 双11前1小时(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("抢购成功,订单正在创建中");
}
}