【架构实战】电商秒杀架构:高并发场景的终极挑战

发布时间:2026/6/21 5:46:18
【架构实战】电商秒杀架构:高并发场景的终极挑战 电商秒杀架构高并发场景的终极挑战一、什么是秒杀系统秒杀是电商平台常见的营销活动商家以极低价格限量售卖商品用户在同一时间集中抢购具有瞬时高并发、库存少、读写频繁的特点。比如某品牌手机新品首发1000台手机在1秒内被10万用户抢购这就是典型的秒杀场景。秒杀系统的核心挑战有四个瞬时高并发瞬间QPS可能是平时的100倍以上普通架构无法承受库存一致性要避免超卖卖的数量超过库存和少卖库存没卖完系统稳定性不能被秒杀流量冲垮影响其他正常业务用户体验响应要快不能出现长时间等待或错误。2019年某电商平台的秒杀活动中就因为架构设计缺陷出现了超卖1200台手机的严重事故最终赔付用户超过200万元还影响了平台口碑。接下来我们就从架构演进的角度拆解秒杀系统的设计要点和避坑指南。二、秒杀系统架构演进从单机到分布式1. 单机秒杀架构早期阶段早期的秒杀系统往往和正常订单系统共用架构流程如下用户请求 → Nginx → Tomcat → 查库存DB → 扣库存DB → 生成订单这种架构的问题非常明显所有请求直接打到数据库秒杀开始时DB的QPS瞬间飙升到几万直接被打死没有限流措施所有用户请求都能进来服务器线程池很快耗尽库存扣减没有并发控制超卖问题严重。当时我们团队做第一个秒杀活动时就用了这种架构结果活动开始3秒后数据库就宕机了库存1000件的商品超卖了237件最后只能用拉黑用户、补偿优惠券的方式收场。2. 分布式秒杀架构成熟阶段经过多次踩坑我们逐步演进出了分布式秒杀架构整体分为四层前端层、网关层、服务层、数据层每层都有对应的优化手段。1前端层优化静态化秒杀页面、商品详情、JS/CSS都放到CDN用户请求直接从最近的CDN节点返回不回源到服务器按钮置灰秒杀开始前购买按钮置灰防止用户重复点击同时前端做限流单个用户1秒内只能发送1次请求答题/验证码增加秒杀门槛防止脚本刷单同时拉长请求时间削峰填谷。2网关层优化限流Gateway层做全局限流比如总QPS限制在5万超过的直接返回活动太火爆请稍后重试鉴权校验用户登录状态、账号是否正常过滤非法请求IP限流单个IP每秒最多5次请求防止单个用户用多个账号刷单。3服务层优化核心Redis预减库存先把库存同步到Redis所有扣库存操作先在Redis中执行避免直接打DBMQ异步下单Redis预减库存成功后把订单信息发送到MQ由消费者异步生成订单、扣减DB库存幂等设计防止MQ消息重复消费导致超卖每个订单请求带唯一RequestId消费前校验是否已处理。4数据层优化库存表设计库存表和订单表分开库存表用行锁或乐观锁避免并发扣减问题DB限流数据库连接池设置最大连接数超过的连接排队等待读写分离普通查询走从库扣库存走主库。三、核心设计库存扣减方案库存扣减是秒杀系统的核心既要保证一致性又要保证高性能。我们采用的是Redis预减库存 MQ异步落库的方案流程如下1. 用户请求到达服务层 2. 查Redis库存如果库存 0直接返回已售罄 3. Redis预减库存用DECR命令扣减库存返回扣减后的库存值 4. 如果扣减后库存 0说明抢购成功发送订单消息到MQ 5. 如果扣减后库存 0说明抢购失败用INCR回滚库存返回未抢到 6. MQ消费者收到消息校验DB库存生成订单扣减DB库存1. Redis预减库存代码实现Java Spring Data RedisServicepublicclassSeckillService{AutowiredprivateStringRedisTemplateredisTemplate;AutowiredprivateRabbitTemplaterabbitTemplate;// 秒杀商品库存Key前缀privatestaticfinalStringSECKILL_STOCK_KEYseckill:stock:;publicSeckillResultseckill(LonguserId,LonggoodsId){StringstockKeySECKILL_STOCK_KEYgoodsId;// 1. 先查库存如果0直接返回StringstockStrredisTemplate.opsForValue().get(stockKey);if(stockStrnull||Integer.parseInt(stockStr)0){returnnewSeckillResult(false,已售罄);}// 2. Redis预减库存DECR是原子操作避免并发问题LongremainStockredisTemplate.opsForValue().decrement(stockKey);// 3. 扣减后库存0说明没抢到回滚库存if(remainStock0){redisTemplate.opsForValue().increment(stockKey);returnnewSeckillResult(false,未抢到请重试);}// 4. 抢购成功生成唯一RequestId发送消息到MQStringrequestIdUUID.randomUUID().toString();SeckillOrderMessagemessagenewSeckillOrderMessage();message.setRequestId(requestId);message.setUserId(userId);message.setGoodsId(goodsId);message.setQuantity(1);rabbitTemplate.convertAndSend(seckill.order.queue,message);returnnewSeckillResult(true,抢购成功订单处理中);}}2. MQ异步下单代码实现ComponentRabbitListener(queuesseckill.order.queue)publicclassSeckillOrderConsumer{AutowiredprivateOrderServiceorderService;AutowiredprivateSeckillGoodsServiceseckillGoodsService;// 用Redis存储已处理的RequestId实现幂等AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringSECKILL_REQ_ID_KEYseckill:req:id:;RabbitHandlerpublicvoidprocess(SeckillOrderMessagemessage){StringrequestIdmessage.getRequestId();StringreqIdKeySECKILL_REQ_ID_KEYrequestId;// 幂等校验如果RequestId已存在说明已经处理过直接返回BooleanexistsredisTemplate.opsForValue().setIfAbsent(reqIdKey,1,10,TimeUnit.MINUTES);if(Boolean.FALSE.equals(exists)){log.info(请求已处理requestId{},requestId);return;}try{// 1. 校验DB库存SeckillGoodsgoodsseckillGoodsService.getById(message.getGoodsId());if(goods.getStock()0){log.warn(DB库存不足goodsId{},message.getGoodsId());return;}// 2. 生成订单OrderordernewOrder();order.setUserId(message.getUserId());order.setGoodsId(message.getGoodsId());order.setQuantity(message.getQuantity());order.setStatus(OrderStatus.PENDING_PAY);orderService.save(order);// 3. 扣减DB库存乐观锁防止并发超卖booleansuccessseckillGoodsService.decreaseStockWithOptimisticLock(message.getGoodsId(),message.getQuantity(),goods.getVersion()// 版本号用于乐观锁);if(!success){// 扣减失败回滚订单orderService.updateStatus(order.getId(),OrderStatus.FAILED);log.warn(DB库存扣减失败goodsId{},message.getGoodsId());}}catch(Exceptione){log.error(处理秒杀订单异常requestId{},requestId,e);// 异常时删除RequestId允许重试redisTemplate.delete(reqIdKey);}}}3. 乐观锁扣减DB库存SQLUPDATEseckill_goodsSETstockstock-#{quantity},versionversion1WHEREid#{goodsId}ANDstock#{quantity}ANDversion#{version}四、秒杀系统踩坑实录1. 超卖问题乐观锁没加库存校验2020年我们第一次用分布式秒杀架构时就出现了超卖问题。当时1000件秒杀商品最终卖出了1037件。排查后发现DB扣减库存的SQL没有加stock #{quantity}的校验只加了版本号乐观锁。当多个线程同时扣减库存时虽然版本号更新了但库存可能已经变成负数导致超卖。修复方法很简单在UPDATE语句中加上stock #{quantity}的条件只有库存足够时才扣减。同时Redis预减库存时也要严格校验扣减后库存为负数的要立即回滚。2. 缓存击穿热点商品Key过期2021年双十一秒杀活动中某款爆款手机的秒杀Key突然过期导致瞬间10万请求直接打到数据库数据库直接被打死秒杀活动被迫中断15分钟。这就是典型的缓存击穿问题热点Key过期后大量请求同时查询缓存发现Key不存在都去查询数据库导致数据库压力骤增。解决方法热点Key不过期秒杀期间热点商品的库存Key设置为不过期活动结束后再删除互斥锁当缓存不存在时用Redis的SETNX命令获取锁只有获取到锁的线程才能查询DB并回写缓存其他线程等待重试随机TTL给不同的Key设置随机的过期时间避免大量Key同时过期。3. Redis主从切换导致库存不一致2022年的一次秒杀活动中Redis主节点突然宕机哨兵自动切换到从节点但此时主节点已经预减了500件库存从节点还没有同步到这个数据导致切换后Redis库存比实际多500件最终超卖了500件。这个问题的根源是Redis主从复制是异步的主节点的写操作不会等待从节点同步完成就返回切换时会有数据丢失。解决方法用Redis集群Redis Cluster是分片存储每个分片有主从主节点写入后至少等待1个从节点同步完成再返回wait命令库存校验消费者处理MQ消息时除了校验DB库存还要再查一次Redis库存两个都满足才生成订单活动前检查秒杀活动开始前手动同步一次主从数据确保从节点数据和主节点一致。4. MQ消息丢失导致库存不一致2023年的一次秒杀活动中RabbitMQ集群的某个节点宕机导致120条秒杀订单消息丢失。这些消息对应的Redis库存已经扣减但DB库存没有扣减导致最终库存显示还有120件但实际已经卖完了用户下单后一直无法支付投诉量激增。解决方法MQ持久化队列、消息都设置为持久化Broker重启后消息不丢失生产者确认开启RabbitMQ的Confirm机制生产者发送消息后等待Broker确认如果失败则重试消费者手动ACK关闭自动ACK消费者处理完消息后再手动ACK处理失败则不ACK消息会重新投递死信队列处理失败的消息放到死信队列人工排查处理。五、业务场景某电商平台秒杀架构完整演进接下来以我参与的一家电商平台的秒杀架构演进为例完整还原从单机到分布式的整个过程。1. 第一阶段单机架构2018年当时平台刚起步秒杀活动最多1000 QPS用单机架构1台Nginx2台Tomcat1个MySQL实例库存直接存在MySQL扣库存用synchronized关键字加锁结果QPS到800时数据库CPU就满了超卖严重经常宕机。2. 第二阶段引入Redis缓存2019年随着用户量增长QPS到了5000引入Redis库存同步到Redis扣库存先操作Redis用DECR原子操作数据库做乐观锁扣减解决超卖问题问题所有请求还是打到TomcatTomcat线程池经常满而且没有限流流量大的时候整个系统都慢。3. 第三阶段引入MQ异步化2020年QPS到了2万引入RabbitMQRedis预减库存成功后发送消息到MQ异步生成订单Tomcat只需要处理Redis预减库存响应时间从500ms降到50ms问题Redis主从切换偶尔会导致库存不一致MQ消息偶尔丢失。4. 第四阶段完整分布式架构2021年至今QPS到了10万演进为完整分布式架构前端CDN静态化 按钮置灰 答题验证码网关Spring Cloud Gateway限流 鉴权 IP限流服务层Redis Cluster RocketMQ替代RabbitMQ更可靠 幂等设计数据层MySQL主从 读写分离 库存表分库分表监控Prometheus Grafana监控QPS、库存、MQ消息堆积异常自动告警。目前这套架构支持过单场秒杀20万QPS零超卖系统可用性99.99%没有出现过大故障。六、秒杀系统核心优化点总结优化点实现方案解决什么问题前端优化CDN静态化、按钮置灰、答题验证码减少无效请求削峰填谷网关限流全局QPS限流、IP限流、用户限流防止流量冲垮系统Redis预减库存DECR原子操作、热点Key不过期减少DB压力快速响应用户MQ异步下单持久化、Confirm机制、手动ACK削峰填谷提升系统吞吐量幂等设计唯一RequestId、Redis去重防止重复下单避免超卖乐观锁扣库存版本号库存校验保证DB库存一致性防止超卖缓存优化互斥锁、随机TTL、热点Key保护防止缓存击穿、雪崩Redis高可用Redis Cluster、主从同步优化防止库存不一致避免超卖七、总结秒杀系统是高并发场景的试金石涵盖了前端、网关、服务、缓存、消息队列、数据库等多个领域的技术点。架构演进的核心思路是分层过滤、逐层限流、异步化、最终一致性。从单机到分布式的演进过程中我们踩过超卖、缓存击穿、库存不一致、消息丢失等各种坑也总结出了一套成熟的架构方案。对于要做秒杀系统的团队建议从一开始就采用分布式的架构思路不要等流量上来再重构升级的成本会高很多。最后记住秒杀系统的目标不是支持无限高的并发而是在保证系统稳定的前提下尽可能多的处理用户请求同时保证库存一致性和用户体验。