Spring Boot + Kafka + Redis 实现电商秒杀系统:高并发场景下的技术深度解析
Spring Boot + Kafka + Redis 实现电商秒杀系统:高并发场景下的技术深度解析
一、业务背景与技术挑战
在大型电商平台(如双11)中,秒杀活动是典型的高并发、低延迟、强一致性的业务场景。瞬时流量可达数十万QPS,而库存仅数百件。若处理不当,将导致超卖、数据库雪崩、服务宕机等问题。
核心挑战包括:
- 超卖问题:多个请求同时读取库存为1,均判断可扣减,导致库存变为-1;
- 数据库压力:直接穿透到MySQL,单库难以承载峰值写入;
- 响应延迟:同步扣减+DB写入耗时长,用户感知卡顿;
- 服务可用性:热点商品请求集中,易引发线程池打满、OOM。
二、整体架构设计
采用分层解耦架构:
用户请求 → Nginx → Spring Boot网关 → Redis缓存校验 → Kafka异步下单 → MySQL最终落库- 接入层:Nginx限流(漏桶算法)、前端按钮防重点击;
- 服务层:Spring Boot 3.2 + Jakarta EE 9,基于Spring WebFlux非阻塞模型提升吞吐;
- 缓存层:Redis Cluster + Lua脚本实现原子扣减与分布式锁;
- 消息层:Kafka 3.6,
seckill-order-topic分区数=32,启用幂等生产者与事务; - 存储层:MySQL 8.0 + ShardingSphere分库分表,库存字段使用
version乐观锁。
三、关键技术实现与代码案例
3.1 Redis预减库存(Lua脚本保证原子性)
// RedisTemplate执行Lua脚本 String script = "local stock = redis.call('GET', KEYS[1])\n" + "if not stock or tonumber(stock) <= 0 then\n" + " return -1\n" + "end\n" + "local result = redis.call('DECR', KEYS[1])\n" + "if result < 0 then\n" + " redis.call('INCR', KEYS[1])\n" + " return -1\n" + "end\n" + "return result"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList("seckill:stock:" + skuId), new Object[]{});✅ 原子性保障:避免先GET再DECR的竞态条件; ❌ 禁用
@Cacheable注解:因缓存穿透风险,必须显式控制缓存生命周期。
3.2 Kafka异步下单(解耦库存校验与订单生成)
// 生产者:发送SeckillOrderEvent事件 kafkaTemplate.send("seckill-order-topic", SeckillOrderEvent.builder() .orderId(UUID.randomUUID().toString()) .skuId(skuId) .userId(userId) .timestamp(System.currentTimeMillis()) .build()); // 消费者:@KafkaListener(topics = "seckill-order-topic") @Transactional // 在消费者端开启本地事务,确保DB写入与offset提交原子性 public void onOrderEvent(SeckillOrderEvent event) { // 1. 再次校验库存(防缓存失效/网络重试) if (!redisTemplate.hasKey("seckill:stock:" + event.getSkuId())) { throw new RuntimeException("库存已售罄"); } // 2. 插入订单(MySQL) orderMapper.insert(toOrderPO(event)); // 3. 扣减最终库存(乐观锁) int updated = stockMapper.decrStockWithVersion(event.getSkuId(), 1); if (updated == 0) { throw new RuntimeException("超卖异常:版本号不匹配"); } }3.3 分布式锁优化(Redisson + RLock)
// 防止缓存击穿:对空库存结果也加锁 RLock lock = redissonClient.getLock("seckill:lock:" + skuId); try { if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 查询DB确认库存,若为0则写入空值缓存(Cache Aside Pattern) Integer dbStock = stockMapper.selectStock(skuId); if (dbStock == null || dbStock <= 0) { redisTemplate.opsForValue().set( "seckill:stock:" + skuId, "0", 2, TimeUnit.MINUTES); } } } finally { if (lock.isHeldByCurrentThread()) lock.unlock(); }四、监控与可观测性(Spring Boot Actuator + Micrometer + Prometheus)
- 自定义MeterRegistry统计秒杀成功率:
Counter.builder("seckill.attempt") .tag("result", "success") .register(meterRegistry);- Grafana看板配置:实时QPS、Redis命中率、Kafka消费延迟、MySQL慢SQL TOP10。
五、面试官视角:为什么这样设计?
Q1:为何不用MySQL行锁解决超卖? A:InnoDB行锁在高并发下会升级为表锁,且锁等待导致RT飙升;Redis内存操作微秒级,性能提升百倍。
Q2:Kafka消息丢失如何保证? A:生产者设置
acks=all+retries=Integer.MAX_VALUE,消费者手动提交offset(enable.auto.commit=false),配合DLQ死信队列兜底。Q3:如果Redis集群故障,如何降级? A:通过Sentinel监听Redis状态,自动切换至本地Caffeine缓存(TTL=1s)+MySQL悲观锁兜底,牺牲一致性保可用性。
【小白学习指南】业务场景与技术点总结
| 场景环节 | 技术点 | 学习要点 | |----------------|-------------------------|--------------------------------------------------------------------------| | 用户抢购入口 | Spring WebFlux + Reactor | 非阻塞IO模型对比Servlet容器,理解背压(Backpressure)机制 | | 库存预校验 | Redis Lua脚本 | 为什么不能用MULTI/EXEC?——Lua在Redis单线程内原子执行,避免网络往返开销 | | 异步下单 | Kafka事务 + 幂等生产者 |transaction.id如何保证Exactly-Once语义?需配合Consumer端isolation.level=read_committed| | 最终一致性 | Saga模式(补偿事务) | 若订单插入成功但库存扣减失败,触发CancelOrderSaga回滚订单(需设计反向SQL) | | 容灾降级 | Sentinel熔断规则 | 配置QPS阈值5000,超阈值自动返回HTTP 429 Too Many Requests并记录日志 |
💡 进阶思考:结合Resilience4j实现
RateLimiter与CircuitBreaker组合策略;用OpenFeign调用风控服务实时拦截黑产IP。
最后,面试官微笑合上简历:“方案很扎实,细节也考虑周全。我们会在3个工作日内通知您后续流程,请保持手机畅通。”
🌟 文章配套GitHub仓库:https://github.com/yourname/seckill-demo (含Docker Compose一键部署Kafka+Redis+MySQL)
