Spring Boot微服务架构下的电商秒杀系统设计与实现
Spring Boot微服务架构下的电商秒杀系统设计与实现
1. 业务背景与挑战
在大型电商平台(如双11)中,秒杀活动是典型的高并发、低延迟、强一致性的业务场景。瞬时流量可达数十万QPS,而库存仅数百件。若处理不当,极易引发:
- 数据库连接池耗尽
- 库存超卖(多线程/分布式环境下)
- 系统雪崩(下游服务被压垮)
- 用户体验差(长时间等待或频繁失败)
2. 技术选型与架构设计
核心技术栈
- Web框架:Spring Boot 3.2 + Spring WebFlux(响应式非阻塞)
- 缓存层:Redis Cluster + Lua脚本(原子扣减库存)
- 消息队列:Apache Kafka(异步下单、解耦库存扣减与订单生成)
- 数据库:MySQL 8.0 + ShardingSphere-JDBC(分库分表)
- 限流降级:Resilience4j(实时熔断+RateLimiter)
- 监控告警:Micrometer + Prometheus + Grafana(QPS、RT、错误率、Redis命中率)
整体架构图(文字描述)
用户请求 → Nginx(静态资源+负载均衡)
↓
Spring Cloud Gateway(JWT鉴权 + 全局限流)
↓
[秒杀服务] → Redis Lua扣库存(预减)→ 成功则发Kafka消息
↓
[Kafka Consumer] → 异步创建订单 → MySQL持久化 → 发送MQ通知物流服务
↓
[降级兜底] → 若Redis/Kafka异常,自动切换至本地内存令牌桶 + 延迟队列重试
3. 关键代码实现
3.1 Redis Lua原子扣减库存(防止超卖)
// Lua脚本:seckill.lua
-- KEYS[1]: 商品ID, ARGV[1]: 请求数量
if redis.call('exists', 'seckill:stock:' .. KEYS[1]) == 0 then
return -1 -- 商品未初始化
end
local stock = tonumber(redis.call('get', 'seckill:stock:' .. KEYS[1]))
if stock < tonumber(ARGV[1]) then
return 0 -- 库存不足
end
redis.call('decrby', 'seckill:stock:' .. KEYS[1], ARGV[1])
return 1 -- 扣减成功
@Service
public class SeckillService {
@Autowired private RedisTemplate<String, Object> redisTemplate;
@Autowired private DefaultRedisScript<Long> seckillScript;
public boolean trySeckill(Long skuId, Integer quantity) {
String scriptPath = "seckill.lua";
Long result = redisTemplate.execute(
seckillScript,
Collections.singletonList("seckill:stock:" + skuId),
quantity.toString()
);
return result != null && result == 1L;
}
}
3.2 Spring WebFlux响应式秒杀接口(非阻塞)
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@PostMapping("/{skuId}/execute")
public Mono<ResponseEntity<SeckillResult>> execute(
@PathVariable Long skuId,
@RequestBody SeckillRequest request,
@RequestHeader("X-User-ID") String userId) {
return Mono.fromCallable(() -> {
// 1. 预校验(本地缓存+布隆过滤器防缓存穿透)
if (!bloomFilter.mightContain(skuId)) {
throw new BusinessException("商品不存在");
}
// 2. Redis Lua扣库存
boolean success = seckillService.trySeckill(skuId, request.getQuantity());
if (!success) {
throw new BusinessException("库存不足");
}
// 3. 发送Kafka消息(异步下单)
kafkaTemplate.send("seckill-order-topic",
new SeckillOrderEvent(userId, skuId, request.getQuantity()));
return new SeckillResult(true, "秒杀成功,订单已入队");
})
.onErrorResume(BusinessException.class, e ->
Mono.just(new SeckillResult(false, e.getMessage())))
.map(result -> ResponseEntity.ok(result))
.timeout(Duration.ofSeconds(2),
Mono.just(ResponseEntity.status(408).body(new SeckillResult(false, "请求超时"))));
}
}
3.3 Resilience4j熔断配置(application.yml)
resilience4j.circuitbreaker:
instances:
seckillService:
failure-rate-threshold: 50
minimum-number-of-calls: 100
wait-duration-in-open-state: 60s
permitted-number-of-calls-in-half-open-state: 10
resilience4j.ratelimiter:
instances:
seckillApi:
limit-for-period: 1000
limit-refresh-period: 1s
timeout-duration: 0
4. 面试官视角:三轮递进式提问(附答案)
🔹 第一轮:基础验证(考察技术广度)
- 为什么秒杀要用Redis而不是直接查DB?
- Kafka在这里起什么作用?如果不用MQ会有什么问题?
- WebFlux和传统Spring MVC对比,优势在哪?适用哪些场景?
🔹 第二轮:深度设计(考察架构能力)
- 如何保证Redis扣减库存的原子性?Lua脚本是否100%可靠?
- 如果Kafka消费者挂了,订单丢失怎么办?如何实现Exactly-Once语义?
- 用户重复提交(F5刷新)如何拦截?前端防抖+后端幂等怎么设计?
🔹 第三轮:生产落地(考察工程素养)
- 如何监控秒杀链路的性能瓶颈?请画出关键指标看板(Prometheus+Grafana)
- 大促前压测发现Redis CPU飙升,可能原因有哪些?如何优化?
- 如果老板要求“零超卖”,但技术方案无法100%保证,你如何沟通并推动风控方案?
✅ 附:问题答案详解(小白也能懂)
Q1:为什么秒杀要用Redis而不是直接查DB? ✅ 答:MySQL单机QPS约3000,而秒杀峰值常达10w+。Redis内存操作QPS可达10w+,且支持原子操作(INCR/DECR/Lua)。先用Redis做“第一道闸门”,把99%无效请求挡在外面,再让真实订单进入DB,保护核心数据库。
Q2:Kafka的作用?不用MQ的问题? ✅ 答:Kafka解耦“秒杀请求”和“订单落库”。若同步写DB,用户需等待数秒;而Kafka异步化后,用户200ms内返回成功。不用MQ会导致:① 接口RT飙升 ② DB瞬间被打爆 ③ 无法平滑扩容消费者(DB写压力无法水平扩展)。
Q3:WebFlux vs Spring MVC? ✅ 答:传统MVC基于Servlet线程模型(每个请求占一个Tomcat线程),高并发时线程池耗尽;WebFlux基于Netty事件驱动,少量线程可处理数万连接。适合I/O密集型场景(如调用Redis/Kafka),但CPU密集型(如复杂计算)仍推荐MVC。
Q4:Lua脚本是否100%可靠? ✅ 答:是。Redis是单线程执行Lua,脚本内所有命令原子执行。但要注意:① 脚本不能过长(避免阻塞其他命令)② 必须预热(首次加载有JIT开销)③ 集群模式下KEY必须落在同一slot(用{}哈希标签)。
Q5:Kafka消费者挂了,订单丢失? ✅ 答:通过Kafka事务+DB本地事务表实现“最终一致性”。消费者消费消息后,先写DB订单表(status=PROCESSING),再提交offset;定时任务扫描status=PROCESSING超时订单,触发重试。配合死信队列(DLQ)人工介入。
Q6:防重复提交? ✅ 答:前端加按钮置灰+防抖;后端用Redis SetNX生成唯一请求ID(request_id),每次请求携带该ID,服务端校验ID是否存在——存在则拒绝(幂等)。ID有效期设为订单超时时间(如30分钟)。
Q7:关键监控指标? ✅ 答:Grafana看板应包含:① QPS/RT/P99(网关层)② Redis命中率/evicted_keys(缓存健康度)③ Kafka lag(消费者积压)④ DB connection pool usage(连接池使用率)⑤ CircuitBreaker状态(熔断开关)。
Q8:Redis CPU飙升? ✅ 答:常见原因:① 大Key(如一个Hash存百万商品库存)→ 改用分片Key(seckill:stock:{skuId})② 慢查询(KEYS *)→ 禁用危险命令,改用SCAN ③ 客户端连接泄漏→ 检查Lettuce连接池配置(maxIdle/minIdle/maxCreate)。
Q9:如何沟通“零超卖”? ✅ 答:技术上无法绝对零超卖(网络分区、硬件故障等极端情况),但可通过“三重保障”将概率降至百万分之一:① Redis预减(主流程)② DB乐观锁更新(二次校验)③ 对账服务(T+1小时比对Redis与DB库存,自动补偿)。向老板说明技术边界,并提供SOP应急方案(如超卖后发放优惠券补偿)。
5. 总结
电商秒杀不是炫技,而是对工程能力的终极考验。它要求开发者: 🔹 深刻理解技术原理(如Redis单线程、Kafka分区机制) 🔹 具备全链路视角(从前端防抖到DB对账) 🔹 拥有风险意识(永远假设网络会断、磁盘会坏、代码有bug)
“好的架构不是设计出来的,而是在一次次大促血泪中演进出来的。”
本文代码已在GitHub开源:https://github.com/example/seckill-springboot
更多推荐
所有评论(0)