1. 核心挑战秒杀系统的本质秒杀Seckill场景如双11、抢票本质上是在极短时间内处理海量并发请求并对有限资源进行正确性扣减。1.1 三大核心痛点高并发读取瞬时QPS每秒查询率可达百万级远超常规系统。高并发写入库存扣减是“竞争资源”数据库成为瓶颈。数据一致性不能超卖卖出的比库存多也不能少卖库存扣减失败。1.2 量级估算假设一场秒杀100万用户抢购1000件商品。如果没有防护数据库连接池瞬间会被打满。网络带宽、CPU、内存都会在毫秒级达到峰值。2. 架构演进从单机到分布式2.1 初级阶段单体应用 数据库行锁架构Nginx Web应用 MySQL。逻辑update goods set stockstock-1 where id1 and stock0。问题数据库连接数有限1万并发就能压垮数据库。行锁虽然保证了数据正确性但事务并发能力极差TPS通常低于1000。2.2 中级阶段缓存拦截 异步队列架构CDN - 分布式缓存Redis Cluster - 消息队列MQ - 数据库。核心将库存预加载到Redis利用Redis的单线程原子性处理扣减。成功者发送MQ异步落库。2.3 高级阶段多级缓存 本地化 容器化弹性伸缩架构采用OpenRestyLua脚本在Nginx层直接读取本地缓存如Caffeine或Redis将流量拦截在最上游。核心利用K8s实现秒级弹性扩容。3. 流量削峰堵不如疏秒杀系统的核心思想是“把请求拦住不让它打到数据库”。3.1 页面静态化与CDN策略秒杀页面商品详情、倒计时、按钮做成纯静态HTML推送到CDN节点。效果99%的静态请求被CDN消化只有点击“立即抢购”的请求才打到后端。3.2 前端限流按钮置灰/验证码策略倒计时客户端时间与服务器时间同步防止提前抢跑。置灰秒杀开始前按钮不可点开始后点击一次后立即置灰防止用户狂点产生大量无效请求。验证码在点击抢购前强制输入验证码或滑块增加计算成本防止机器脚本大规模扫货。3.3 网关层限流Token Bucket / Leaky Bucket策略在Nginx或Spring Cloud Gateway层面配置限流。总流量限制例如只放行10万请求进入后端其余的直接返回“排队中”或“已售罄”。实现使用Nginx的limit_req模块或Sentinel阿里巴巴开源组件。3.4 异步处理消息队列策略一旦用户抢购资格验证通过库存扣减成功不直接操作数据库而是将订单信息发送到MQ。效果后端Worker异步消费MQ批量写入数据库。将“同步写”变为“异步写”极大降低数据库压力。4. 库存扣减精准与超卖的博弈这是秒杀系统的心脏。目标是实现零超卖且高吞吐。4.1 基于Redis的原子扣减Redis是单线程的利用Lua脚本保证原子性是实现库存扣减的标准方案。Lua脚本逻辑lua-- KEYS[1] 库存Key -- KEYS[2] 用户已购Key用于防重 -- ARGV[1] 用户ID -- ARGV[2] 限购数量通常为1 -- ARGV[3] 商品ID -- 1. 检查用户是否已经买过 local bought redis.call(sismember, KEYS[2], ARGV[1]) if tonumber(bought) 1 then return -1 -- 重复抢购 end -- 2. 检查库存 local stock redis.call(get, KEYS[1]) if not stock or tonumber(stock) 0 then return 0 -- 无库存 end -- 3. 扣减库存 redis.call(decr, KEYS[1]) -- 4. 记录购买用户 redis.call(sadd, KEYS[2], ARGV[1]) -- 5. 发送MQ消息注意这里不建议在Lua里直接发而是返回成功状态由应用层发MQ return 1 -- 成功4.2 库存预热与缓存的原子性预热秒杀开始前通过后台任务将商品库存set到Redis。扣减只操作Redis不操作DB。最终一致性Redis扣减成功后发送MQ消费者最终将订单写入DB。如果DB写入失败极少数情况需要有一套对账补偿机制或利用MQ的重试机制。4.3 避免“少卖”如果Redis扣减成功但后续流程MQ发送失败、DB宕机导致订单未生成用户以为抢到了实际没生成订单这叫“少卖”。解决方案使用本地事务表或分布式事务Seata TCC模式。简化版Redis扣减后立即将“下单任务”写入本地MySQL的一张task表状态为“待处理”。异步线程轮询task表执行下单。确保要么Redis回滚要么最终下单成功。5. 风控与防刷拦截机器流量黄牛和脚本是秒杀系统的巨大威胁。风控策略需要层层递进。5.1 设备指纹与IP信誉策略识别同一个IP、同一个设备ID的请求频率。实现利用Nginx的limit_req_zone按IP限流例如每秒5次。如果超过阈值直接返回429 Too Many Requests。5.2 令牌/验证码挑战简单验证码容易被OCR破解。高级验证滑块拼图、点选文字、无感验证基于行为轨迹分析。关键点验证码校验通常放在Redis扣减之前作为一道坚实的“护城河”。5.3 动态URL策略秒杀开始前真正的秒杀接口URL是加密且不暴露的。流程用户点击抢购 - 请求获取秒杀路径 - 服务端校验时间、用户状态 - 返回动态URL如/seckill/{md5(uidskuIdtime)}。防止脚本提前构造请求轰炸。6. 高可用保障降级、限流与熔断秒杀系统是典型的“宁可少卖不可宕机”。6.1 限流阈值设置根据压测结果设置系统的最大承载阈值。层级前端限流按钮置灰。Nginx限流连接数限制、IP限流。业务层限流Sentinel或Hystrix限制每秒处理1000个请求多余的快速失败。6.2 降级策略当秒杀进行中如果发现依赖服务如库存服务、价格服务响应缓慢自动降级。降级方案返回兜底数据如“活动火爆请稍后再试”或者直接关闭非核心功能如评论、推荐。6.3 隔离线程池隔离将秒杀业务单独分配一个线程池与普通业务隔离。防止秒杀流量占满所有Tomcat线程导致普通业务也无法访问。服务器隔离为秒杀活动单独部署一组服务集群物理隔离。7. 数据库与缓存的高阶设计7.1 数据库优化表结构秒杀订单表通常使用水平分表按用户ID或订单ID取模避免单表数据过大。索引确保order_id、user_id、sku_id有合理索引。写入优化MQ消费者采用批量Insert例如每100条插入一次极大提升数据库写入效率。7.2 缓存架构多级缓存L1本地缓存Caffeine/Guava Cache存储在应用服务器内存中。存储“秒杀活动配置”、“商品基本信息”。因为秒杀场景读多写少本地缓存能极大减少Redis的QPS。注意本地缓存存在数据不一致问题通过消息广播Redis Pub/Sub清除本地缓存。L2分布式缓存Redis Cluster。存储库存、用户购买记录。7.3 热点数据隔离问题一个爆品可能导致Redis中某个Key的访问量极高导致Redis单节点CPU飙升。解决拆分库存将1000件库存拆分到10个Redis Key上如stock_1~stock_10请求随机路由到不同Key分摊压力。读写分离Redis Cluster中将读请求分流到从节点。8. 前端与客户端优化8.1 接口聚合不要一次请求获取所有数据。秒杀页面静态化动态数据库存剩余、倒计时通过单独的、轻量级的接口轮询或WebSocket推送。8.2 轮询策略不要使用高频setInterval请求库存。使用长轮询或WebSocket或者采用指数退避策略请求失败后等待时间加倍。8.3 “排队中”体验当用户点击抢购后端返回202 Accepted前端显示“排队中...”。通过轮询或WebSocket获取最终结果成功/失败。这样可以将用户的心理等待时间拉长缓解服务器瞬时压力。9. 监控与全链路压测没有压测的秒杀系统是“裸奔”。9.1 全链路压测工具JMeter、Locust、阿里云PTS。数据隔离压测流量需要标记如header标记is_test: true压测数据写入影子表order_2023对应影子表order_shadow不影响线上真实数据。9.2 关键指标监控QPS/TPS入口流量、Redis QPS、DB TPS。响应时间RTResponse Time TP99、TP999。系统资源CPU、内存、网络IO、磁盘IO。错误率超时率、限流拒绝率、系统异常率。9.3 日志监控ELK实时分析错误日志快速定位代码问题。10. 最佳实践总结与代码示例LuaRedis10.1 最佳实践口诀尽早上游拦截CDN Nginx 本地缓存 Redis MQ DB。读写分离读多写少缓存抗读MQ抗写。原子操作库存扣减必须原子Lua。快速失败与其让用户等待超时不如立即返回“已售罄”或“排队中”。最终一致性不要强求实时写入DB通过MQ异步保证最终一致性。10.2 核心代码示例Spring Boot Redis RocketMQ1. 初始化库存Preheatjava// 秒杀开始前通过Admin接口预热 public void preheatSeckill(Long skuId, Integer stock) { String stockKey seckill:stock: skuId; String userSetKey seckill:users: skuId; redisTemplate.opsForValue().set(stockKey, stock); redisTemplate.delete(userSetKey); // 清空购买记录 }2. 秒杀核心接口Service层javaService public class SeckillService { Autowired private RedisTemplateString, String redisTemplate; Autowired private RocketMQTemplate mqTemplate; // 加载Lua脚本 private DefaultRedisScriptLong seckillScript; PostConstruct public void init() { seckillScript new DefaultRedisScript(); seckillScript.setScriptText(SECKILL_LUA); // 上文提到的Lua脚本 seckillScript.setResultType(Long.class); } public Result seckill(Long userId, Long skuId) { String stockKey seckill:stock: skuId; String userKey seckill:users: skuId; // 执行Lua脚本 Long result redisTemplate.execute(seckillScript, Arrays.asList(stockKey, userKey), String.valueOf(userId), 1, String.valueOf(skuId)); if (result null || result 0) { return Result.error(已售罄); } if (result -1) { return Result.error(您已抢购过啦); } // 抢购成功发送MQ异步创建订单 SeckillOrderMsg msg new SeckillOrderMsg(userId, skuId); mqTemplate.syncSend(seckill_order_topic, msg); return Result.success(抢购成功正在处理订单); } }3. MQ消费者异步落库javaComponent RocketMQMessageListener(topic seckill_order_topic, consumerGroup seckill_group) public class SeckillOrderConsumer implements RocketMQListenerSeckillOrderMsg { Autowired private OrderMapper orderMapper; Override public void onMessage(SeckillOrderMsg msg) { // 幂等性校验基于userIdskuId的唯一索引 // 插入订单表 Order order new Order(); order.setUserId(msg.getUserId()); order.setSkuId(msg.getSkuId()); // ... 其他字段 orderMapper.insert(order); // 更新数据库库存可选如果Redis是唯一权威DB只记录流水可以不做库存扣减 // 如果DB需要记录库存变化可以使用乐观锁 update stock set versionversion1 where sku_id? and version? } }10.3 数据库最终一致性兜底为了防止Redis扣减成功但MQ丢失消息导致少卖可以在Redis扣减成功后将成功记录写入一张本地日志表seckill_success_log异步任务扫描日志表进行补偿创建订单。结语设计一个高可用的秒杀系统本质上是在资源CPU、内存、带宽、数据库连接与流量之间做博弈。核心公式高并发 缓存 异步 限流 原子性缓存把数据放在离用户最近、最快的地方。异步把同步写变成异步写削峰填谷。限流在系统的每一层都设置好安全阀保护核心资源。原子性在分布式环境下保证库存扣减和数据一致性的底线。