Spring Boot微服务架构下的电商秒杀系统设计与实现
Spring Boot微服务架构下的电商秒杀系统设计与实现
一、业务背景与挑战
在大型电商平台(如双11、618)中,秒杀活动是典型的高并发、低延迟、强一致性的业务场景。瞬时流量可达数百万QPS,而库存扣减必须严格保证不超卖、不多卖。传统单体架构+关系型数据库直连的方式极易导致数据库连接池耗尽、行锁竞争激烈、响应超时甚至服务雪崩。
本文以某头部电商大厂真实面试题为蓝本,完整呈现一套基于Spring Boot 3.x(Jakarta EE 9+)、Redis、RabbitMQ与MySQL的云原生秒杀解决方案。
二、技术选型与架构概览
- 核心语言与平台:Java 17 + JVM(G1 GC调优)
- Web框架:Spring Boot 3.2 + Spring WebFlux(非阻塞IO处理预热请求)
- 数据库与ORM:MySQL 8.0(分库分表) + MyBatis-Plus + HikariCP(连接池最小空闲连接=20)
- 缓存技术:Redis Cluster(Lua脚本原子扣减库存)
- 消息队列:RabbitMQ(异步下单、解耦库存扣减与订单生成)
- 安全框架:Spring Security + JWT(防刷接口+用户身份鉴权)
- 监控与运维:Micrometer + Prometheus + Grafana(实时监控QPS、Redis命中率、MQ堆积量)
三、关键代码实现
1. Redis Lua脚本实现原子库存扣减
-- KEYS[1]: 商品ID, ARGV[1]: 扣减数量
if redis.call('exists', KEYS[1]) == 0 then
return -1 -- 库存Key不存在
end
local stock = tonumber(redis.call('get', KEYS[1]))
if stock < tonumber(ARGV[1]) then
return 0 -- 库存不足
end
redis.call('decrby', KEYS[1], ARGV[1])
return 1 -- 扣减成功
2. Spring Boot Controller层(WebFlux响应式)
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired private SeckillService seckillService;
@PostMapping("/try")
public Mono<ResponseEntity<String>> trySeckill(
@RequestBody SeckillRequest request,
@RequestHeader("Authorization") String token) {
return Mono.fromCallable(() -> {
// 1. JWT校验 & 用户风控(滑动窗口限流)
if (!jwtValidator.validate(token)) {
throw new BusinessException("非法访问");
}
// 2. 调用Lua脚本预扣减
Long result = redisTemplate.execute(
new DefaultRedisScript<>("...lua script...", Long.class),
Collections.singletonList(request.getProductId()),
String.valueOf(request.getQuantity())
);
if (result == 1L) {
// 3. 发送MQ消息,异步创建订单
rabbitTemplate.convertAndSend("seckill.order.exchange", "order.route", request);
return ResponseEntity.ok("排队中,请等待结果通知");
} else if (result == 0L) {
return ResponseEntity.status(429).body("库存不足");
} else {
return ResponseEntity.status(500).body("系统异常");
}
}).onErrorResume(BusinessException.class, e ->
Mono.just(ResponseEntity.badRequest().body(e.getMessage()))
);
}
}
3. 消费端订单落库(事务性消息 + 本地事务表)
@Component
public class OrderConsumer {
@RabbitListener(queues = "seckill.order.queue")
public void handleOrder(SeckillRequest request) {
// 1. 先插入本地事务表(状态=PROCESSING)
orderMapper.insertWithTx(request);
// 2. 调用下游订单服务(OpenFeign)
orderService.createOrder(request);
// 3. 更新本地事务表状态=SUCCESS
orderMapper.updateStatus(request.getId(), "SUCCESS");
}
}
四、面试官视角:3轮递进式提问
第一轮(基础原理):
- 为什么不用MySQL乐观锁(version字段)直接扣减库存?
- Redis单线程模型如何支撑百万QPS?Lua脚本为何能保证原子性?
- WebFlux的Mono/Flux与传统Servlet线程模型本质区别是什么?
第二轮(深度设计): 4. 如果MQ消费失败,如何保证最终一致性?请画出事务消息+本地事务表的状态机。 5. 如何防止黄牛使用脚本恶意刷单?请结合Spring Security与前端验证码、设备指纹、行为分析给出方案。 6. 当Redis集群某个节点宕机,库存数据丢失怎么办?是否需要双写DB?
第三轮(高阶扩展): 7. 若将秒杀迁移到Kubernetes环境,如何通过HPA(Horizontal Pod Autoscaler)根据Prometheus指标(如Redis queue length)自动扩缩容? 8. 如何用Resilience4j实现熔断降级?当库存服务不可用时,前端应展示什么友好提示? 9. 假设业务要求支持“阶梯价秒杀”(买得越多单价越低),现有Lua脚本需如何重构?
面试官微笑点头:“很好,今天就到这里。你的系统设计思维和源码细节把握很扎实。我们会在3个工作日内通过邮件通知后续流程——祝你拿到心仪的offer!”
五、答案详解(小白也能懂)
✅ 问题1答案:MySQL乐观锁在高并发下会产生大量失败重试(ABA问题),CPU浪费严重;而Redis Lua在服务端原子执行,无网络往返开销。
✅ 问题4答案:本地事务表是核心。消费者先写表(PROCESSING),再调用订单服务;若失败则定时任务扫描该表,对超时PROCESSING记录重试或告警人工介入,最终达到100%最终一致。
✅ 问题7答案:需自定义Prometheus指标rabbitmq_queue_messages_ready{queue="seckill.order.queue"},配置HPA metrics 使用该指标,当值>1000时触发扩容至10个Pod。
延伸学习建议:动手用Docker Compose一键部署上述全套环境(Redis Cluster + RabbitMQ + MySQL + Spring Boot App),并用JMeter模拟10万并发压测,观察Grafana面板各项指标变化。
更多推荐
所有评论(0)