1. 秒杀场景的技术挑战与设计思路
去年双十一期间,我参与了一个电商平台的秒杀系统重构项目,当时系统峰值QPS达到了惊人的12万。这个过程中,我深刻体会到秒杀场景对系统架构的极端要求。秒杀本质上是一个典型的高并发读和高并发写的场景,需要解决的核心问题可以归纳为以下三点:
首先是库存超卖问题。当10万个用户同时抢购100件商品时,如何确保最终售出的商品不超过100件?这需要一套严谨的库存扣减机制。我们曾经因为一个简单的并发问题,导致超卖了300多件商品,损失惨重。
其次是系统高可用性。在秒杀开始瞬间,流量会瞬间暴涨数百倍。去年我们遇到过一次,由于缓存预热不足,Redis集群直接被流量打挂,整个秒杀活动被迫取消。这种事故对平台信誉的损害是难以估量的。
最后是用户体验的一致性。用户最反感的就是看到"抢购成功"却被告知"库存不足"。我们需要保证前端展示、库存扣减、订单创建这三个环节的强一致性。曾经有竞品因为这个问题被用户集体投诉,教训深刻。
2. Spring Boot在秒杀系统中的角色定位
2.1 微服务架构设计
我们采用Spring Boot作为基础框架,主要看中其快速启动和自动配置的特性。秒杀服务被设计为独立的微服务,与其他业务系统隔离。这里有个关键点:秒杀服务的JVM参数需要特别配置。我们通常会将年轻代大小调整为整个堆的1/3,因为秒杀场景会产生大量短生命周期的对象。
// 典型秒杀服务启动类配置 @SpringBootApplication @EnableCircuitBreaker // 熔断保护 @EnableAsync // 异步处理 public class SeckillApplication { public static void main(String[] args) { new SpringApplicationBuilder(SeckillApplication.class) .web(WebApplicationType.SERVLET) .run(args); } }2.2 接口设计原则
秒杀接口设计有几个黄金法则:
- 保持接口无状态,方便水平扩展
- 使用POST而非GET,避免浏览器预请求
- 接口路径中加入版本号,便于后续升级
- 返回结果要精简,通常只包含必要状态码
@RestController @RequestMapping("/api/v1/seckill") public class SeckillController { @PostMapping("/{activityId}") public ResponseResult seckill( @PathVariable Long activityId, @RequestHeader("userId") Long userId) { // 实现逻辑 } }3. Redis在秒杀中的核心应用
3.1 库存预热与原子扣减
我们采用Redis的Hash结构存储商品库存,键设计为"seckill:stock:{activityId}"。这里有个重要技巧:库存要提前预热,避免活动开始时才加载。我们曾经因此导致Redis连接数暴增,整个集群响应变慢。
// 库存扣减的Lua脚本 String script = "local stock = tonumber(redis.call('HGET', KEYS[1], 'stock')) " + "if stock > 0 then " + " redis.call('HINCRBY', KEYS[1], 'stock', -1) " + " return 1 " + "else " + " return 0 " + "end";3.2 分布式锁的实现
对于需要强一致性的操作,我们使用Redisson的分布式锁。但要注意,锁的粒度要尽可能小,持有时间要尽可能短。曾经有个同事在锁内执行了数据库查询,导致锁竞争严重,系统吞吐量直接降为原来的1/10。
RLock lock = redissonClient.getLock("seckill:lock:" + activityId); try { if (lock.tryLock(100, 10, TimeUnit.MILLISECONDS)) { // 处理核心业务 } } finally { lock.unlock(); }4. Kafka在异步处理中的应用
4.1 流量削峰设计
我们将秒杀成功的消息发送到Kafka,由消费者异步创建订单。这里的关键是分区数的设置:分区数应该等于订单服务的实例数,这样才能最大化并行处理能力。我们曾经设置过少的分区,导致消息堆积严重。
@KafkaListener(topics = "seckill-order", concurrency = "3") public void handleOrderMessage(SeckillMessage message) { // 异步创建订单 }4.2 消息可靠性保证
我们采用本地事务表+定时任务的方式保证消息不丢失。具体做法是:先将消息存入数据库事务表,标记为"待发送",事务提交后再发送到Kafka,最后由定时任务补偿发送失败的消息。这套机制帮助我们实现了99.999%的消息可靠性。
@Transactional public void processSeckill(Long userId, Long activityId) { // 扣减库存 seckillService.reduceStock(activityId); // 记录消息到本地表 messageService.savePendingMessage(userId, activityId); // 发送消息到Kafka kafkaTemplate.send("seckill-order", buildMessage(userId, activityId)); }5. 系统优化实战经验
5.1 缓存设计技巧
我们采用多级缓存策略:本地缓存(Caffeine) + Redis集群。本地缓存用于应对极端热点商品,设置5秒自动过期,避免数据不一致。这里有个坑:曾经有同事设置了过长的本地缓存时间,导致库存显示不准确,引发用户投诉。
// 多级缓存实现示例 public SeckillActivity getActivity(Long activityId) { // 先查本地缓存 SeckillActivity activity = localCache.get(activityId); if (activity == null) { // 查Redis activity = redisTemplate.opsForValue().get("seckill:activity:" + activityId); if (activity != null) { localCache.put(activityId, activity); } } return activity; }5.2 限流与降级策略
我们在网关层实现动态限流,基于Redis实现令牌桶算法。当系统负载达到阈值时,自动开启降级策略,返回静态页面。这里的关键是限流阈值的设置:我们通过压力测试发现,当系统负载达到70%时就应该开始限流,而不是等到90%。
// 基于Redis的令牌桶限流 public boolean tryAcquire(String key, int permits) { long now = System.currentTimeMillis(); RedisScript<Long> script = RedisScript.of(LIMIT_SCRIPT, Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(key), String.valueOf(permits), String.valueOf(now)); return result != null && result == 1L; }6. 面试常见问题解析
6.1 库存超卖问题解决方案
面试官最喜欢问的就是如何解决超卖问题。我的回答通常是分三个层次:
- 前端层面:按钮置灰,限制重复提交
- 中间层:Redis原子操作扣减库存
- 底层:数据库唯一索引+乐观锁
-- 数据库最终保障 UPDATE seckill_goods SET stock = stock - 1 WHERE id = ? AND stock > 06.2 系统性能优化思路
当被问到性能优化时,我会从以下几个维度展开:
- 架构层面:服务拆分、读写分离
- 缓存层面:多级缓存、热点探测
- 代码层面:池化技术、异步化
- JVM层面:GC调优、堆外内存
重要提示:在解释优化方案时,一定要带上具体的数据指标,比如"通过本地缓存,我们将Redis QPS从5万降到了1万"
7. 真实案例:双十一秒杀事故复盘
去年双十一,我们的秒杀系统出现了一个严重问题:在活动开始5分钟后,订单服务突然不可用。经过排查发现是数据库连接池耗尽。根本原因是Kafka消费者并发度设置过高,导致短时间内创建了大量数据库连接。
解决方案:
- 引入HikariCP连接池,设置合理的最大连接数
- 在Kafka消费者中增加批处理逻辑,减少数据库操作频次
- 对订单表进行水平拆分,减轻单表压力
这个事故教会我们:任何中间件的参数配置都需要经过严格的压力测试,不能想当然地设置。
8. 监控与告警体系建设
完善的监控体系是秒杀系统的生命线。我们建立了四层监控:
- 基础层:服务器CPU、内存、磁盘
- 中间件层:Redis、Kafka、数据库
- 应用层:JVM、接口QPS、耗时
- 业务层:库存变化、订单创建成功率
告警策略采用阶梯式:
- 黄色预警:自动扩容
- 橙色预警:限流降级
- 红色预警:人工介入
我们使用Prometheus+Grafana搭建监控大盘,关键指标设置1分钟粒度采集。曾经通过监控提前10分钟发现Redis热点Key问题,避免了重大事故。