资讯动态

100万QPS秒杀架构全解析:从入口拦截到异步落地的系统设计

发布时间:2026/10/8 11:36:52 来源:尧图企业网站定制
兄弟们聊到高并发秒杀很多人第一反应就是“100万QPS”这个数字。我面试过不少候选人简历上写“支撑过百万QPS秒杀”结果一问细节要么支支吾吾要么就是“用了Redis防超卖”一句话带过。说实话100万QPS这个量级不是靠某个中间件或者某段骚代码就能堆出来的它是一整套从入口到数据落地的系统性工程。很多朋友对“高并发”和“秒杀”的理解往往只停留在“Redis缓存MQ削峰”这个概念层面。但真正面对双11零点、某顶流明星限量周边开售这种场景时你会发现哪怕一个毫秒级的GC停顿、一次MySQL主从延迟都可能导致雪崩。所以这篇帖子我想把100万QPS秒杀架构这件事从物理世界的带宽算起一直拆到数据库底层把我踩过的坑、验证过的参数、权衡过的取舍一次性讲透。这篇文章不是给刚入门的朋友看概念而是给那些已经写过CRUD、玩过Redis、被线上故障教育过想真正搞懂大规模高并发架构背后“为什么”的工程师。咱们直接开始。1. 从物理极限到架构蓝图100万QPS意味着什么在看任何架构图之前先做一道小学数学题否则后面全是空中楼阁。1.1 用带宽和TCP连接算的第一笔账100万QPS假设每个请求的请求体加上响应体平均按2KB流量算这已经是极度压缩后的静态化数据量了那么一秒钟需要传输100万请求数 × 2KB双向流量 2GB/s的吞吐量。换算成带宽大概是16Gbps注意大小写。这意味着什么一台普通千兆网卡的物理机1Gbps满打满算也就125MB/s连零头都不到。你需要至少16台物理机的网卡全部打满还不算任何协议开销和转发损耗。也就是说整个机房在那一秒光网卡吞吐量就得预留出20%以上的冗余这还没算负载均衡器之间的内网流量。再看TCP连接。如果是短连接模型每秒新建100万个TCP连接意味着每秒要处理100万次三次握手和四次挥手。Linux内核在默认配置下每秒新建连接数撑死也就5-10万而且新建连接会带来严重的CPU开销和TIME_WAIT堆积。所以100万QPS架构下的第一个铁律就是必须使用长连接或者HTTP/2多路复用彻底砍掉握手成本。1.2 每个环节的真实承压能力算完物理账再看软件栈的承压能力。这是我在压测环境里一点点调出来的经验值不同机器配置会有浮动但量级基本靠谱环节单机/单实例能力参考瓶颈点Nginx/LVS5-10万QPS性能调优后网卡软中断、epoll处理线程Redis单实例10万 QPS读操作pipeline下更高单线程CPU、网络IOMySQL单库3000-8000 QPS简单查询SQL解析、锁竞争、IO刷盘业务容器Java2000-8000 QPS带完整业务逻辑线程上下文切换、GC、数据库连接池看到没数据落地环节和前置环节之间差了整整两个数量级。这决定了你在做架构时必须有一个坚定的信念请求在到达数据库之前能拦截多少就拦截多少能挡回去就挡回去数据库永远只能处理最关键的那笔写操作。1.3 业务场景决定了架构边界还有一点必须想清楚秒杀系统和其他高并发系统不一样的地方在于它追求的不是循序渐进的处理完所有请求而是在极短时间内挡掉99.9%的无效流量只放行真正能买东西的人。因为我做的就是这种瞬时流量峰值的场景所以整个架构设计的核心思想就八个字前置过滤异步落地。后面所有章节的内容都是围绕这八个字展开的。2. 流量漏斗入口层与接入层的分层拦截策略一台物理机撑不住我们就用几十台机器把流量分散开。但入口层的核心不在于把流量分给谁而在于在哪个环节干掉多少流量。2.1 DNS与CDN层把静态流量拦在门外秒杀页面最大的特点就是商品信息、活动规则、倒计时页面这些数据在活动开始前千篇一律跟用户状态无关。这部分流量大概占整体请求的80%以上。我见过最好的静态化方案是把整个秒杀商品页打成静态资源放CDN动态数据通过异步接口单独拉取。CDN层可以直接扛掉地域性的网络延迟把源站的带宽压力释放出来。而且CDN在边缘节点就能根据活动规则把部分爬虫和异常IP挡下来这种拦截发生在离用户最近的地方代价最小。2.2 网关层的令牌桶限流与全局开关到了四层/七层负载均衡这一层就需要做真正的流量控制策略了。我在网关层基于OpenResty做了三件事第一全局限流。用Nginx的limit_req模块实现令牌桶算法全局限流在80万QPS左右超过部分直接返回排队中的降级页面。这里的80万不是随口说的是给后端预留了20%的余量防止突发流量把按100万设计的系统打穿。第二用户维度限流。网关层解析Cookie中的用户标识不用解密只做散列取模把同一个用户的请求频率限制在5秒一次。这么做不是为了限流而是防止网络超时后的自动重试把只属于用户的重复请求给滤掉。第三全局开关和灰度。我做过一个近乎变态的开关设计网关支持通过配置中心动态下发放量比例从0%到100%平滑调整。这样哪怕活动开始后发现问题也不需要重启任何服务直接把放量比例调到10%线上影响面立刻可控。2.3 一个容易忽略的参数TCP的TIME_WAIT与backlog在网关这台机器上有个参数是压测时最先冒出来的问题net.ipv4.tcp_tw_reuse和net.ipv4.ip_local_port_range。如果接入层直接用短连接你会发现压测跑到一半系统莫名其妙开始大量丢包、connect超时用ss -s一看TIME_WAIT把本地端口耗尽了。我通常在压测前就会做这样一组内核参数调整# 注意以下参数仅适用于压测和特定业务场景需要在充分理解TCP状态机的前提下调整 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_tw_recycle 1 # 仅在NAT设备后无用户时使用 net.ipv4.ip_local_port_range 1024 65535 net.core.somaxconn 65535但说句实在话真正解决连接问题的方案是长连接而不是持续去调内核参数。参数只是把体力活变轻巧架构上的长连接才是治本。3. 商品库存的原子性设计在Redis里解决超卖问题秒杀系统的心脏就是库存。库存不能超卖这是商业底线而每秒100万请求要在毫秒级内完成库存校验和扣减传统数据库事务根本扛不住。所以真正的战场在缓存层。3.1 为什么要用Lua脚本保证原子性在Redis中扣减库存最容易犯的错误是先GET再SET。这么干在并发下一定出问题——线程A GET到库存是10还没SET线程B也GET到10两人都把库存储成9超卖的根源就在这里。Redis是单线程执行的它的EVAL命令可以保证一个Lua脚本在执行过程中不被其他命令插入。所以我把库存扣减逻辑写成了这样-- KEYS[1]: 库存key -- ARGV[1]: 购买数量 local stock tonumber(redis.call(GET, KEYS[1])) if not stock or stock tonumber(ARGV[1]) then redis.call(INCRBY, sec:fail: .. KEYS[1], 1) return -1 end redis.call(DECRBY, KEYS[1], tonumber(ARGV[1])) return stock - tonumber(ARGV[1])这段脚本把判断库存是否充足和扣减库存放在同一个原子操作里不管多少并发过来Redis内部都会串行执行彻底消除了超卖的可能。3.2 库存预热与分段存储另一个关于Redis的实践是库存预热。活动开始前把数据库里的库存总量一次性加载到Redis里并设置一个永不过期但是带逻辑时间的Key。活动期间所有库存判断和扣减都在缓存层面完成数据库里的库存字段不做实时更新只是在活动结束后用实际订单数反推覆盖。当100万QPS全部打到同一个库存Key上时这个Key会变成Redis里的热Key单实例Redis可能扛不住那么高的访问量。这时候就需要做分段库存把1万个库存拆分成10个Key每个Key存1000件放入不同的Redis分片。用户请求时经过网关层根据用户ID的哈希值路由到一个具体的库存分片。这10个分片的扣减逻辑完全独立互不干扰。这种分片减库存的设计在双11的多个大促场景里都验证过效果稳定。它带来的副作用是最后一件商品可能在不同分片里同时显示有货但实际扣减时会精确保证总数不超卖用户体验上几乎感知不到差异。3.3 本地缓存兜底给Redis减负的最后一招Redis的性能再高也架不住100万QPS全部穿透到它身上。所以我在业务容器里做了一个本地缓存前置的环节业务容器启动时从Redis把库存数量的变化区间加载到JVM本地内存中比如库存总量5万件的活动本地缓存一个剩余库存的近似值。这个本地缓存的逻辑是如果本地预估库存已经小于0直接返回已售罄不再请求Redis。如果本地预估库存还有余量则把请求转发给Redis执行真实的Lua扣减。这个方案的代价是可能多放进来一些请求但结合后面的异步削峰设计整体系统完全能接受这种误差。它的收益是巨大的——90%的流量在业务容器本地就直接被挡掉了Redis收到的请求量级直接降一个维度。4. 订单落库的异步化改造把100万条写请求压缩成1万条前面说了请求在经过入口拦截、网关限流、本地缓存Redis扣减之后能活着走到创建订单这一步的流量已经很少了。但这部分流量依然是峰值如果每笔订单都同步写数据库MySQL一样死给你看。4.1 为什么不能直接同步写MySQL一次订单INSERT事务粗算下成本SQL解析、检查约束、索引更新、InnoDB的redo log刷盘、binlog记录。顺利的话也要1-2毫秒。1万并发同时写MySQL要处理10-20秒的事务排队连接池最先被打满锁等待让延迟指数级上升。所以永远不要把订单数据直接怼到数据库。正确姿势是先把订单落到消息队列或者一个队列表然后由消费者异步批量刷库。4.2 队列表的细节设计我用过Kafka也用过RocketMQ它们都很成熟。但这里想聊一个更轻量但同样有效的方案——队列表。它也是很多大厂在秒杀场景下的终极兜底方案因为不依赖额外中间件且天然具备事务能力。队列表的结构核心就几个字段字段说明order_id订单号唯一索引user_id用户IDitem_id商品IDstatus状态0待处理1已落库2重试中create_time入队时间retry_count重试次数关键操作逻辑是订单服务在同一个事务里先扣减Redis库存通过Lua再把订单数据INSERT到队列表。这里的order_id是幂等键秒杀场景下同一个用户同一场活动只允许一条记录靠数据库唯一索引保证比在Redis里反复检查强一万倍。4.3 刷库的攒批策略队列表存在的意义就是让下游消费者可以批量处理。我实现过一个典型的批量刷库任务一个后台线程每100毫秒扫描一次队列表把状态为0的数据一次性取1000条出来批量INSERT到订单表。100毫秒批量刷一次算出每秒能处理1万条事务如果并发峰值实在太高就把扫描间隔缩短到50毫秒。这样做的核心理念叫攒批把1000次随机IO合并成一次顺序IO吞吐量直接提升一个数量级。而且队列表本身还用到了MySQL的分区表设计按create_time做RANGE分区。历史数据可以直接TRUNCATE掉不会拖垮查询性能。5. 防作弊、幂等与风控静默拦截掉50%的坏流量说个容易被忽视但真实占比惊人的数据在真正的秒杀活动中至少50%的请求是机器请求或重复请求——脚本、黄牛、爬虫、羊毛党。如果不做风控这些人不仅抢走商品还会让系统的QPS压力翻倍。5.1 用户维度的风控规则引擎风控这件事在架构上的定位是旁路而不是主路。不能因为风控系统的故障导致正常用户无法下单所以风控整条链路都做了降级开关。风控规则的来源分几个层次用户等级与历史行为新注册账号、历史订单异常、收货地址频繁变更这些用户直接打上高危标签。设备指纹同一台设备在短时间内切换多个账号或同一个设备指纹出现在多个城市基本可以判定为黄牛。IP维度同一IP段下用户请求量异常稠密或在短时间内横跨多个城市需要加重校验。这些规则在风控服务里计算打上标签后异步推给网关层。网关层在放行前查一下标签如果是高危用户直接返回活动太火爆啦而不进入后面的业务链路。5.2 幂等的另一种表达数据库唯一索引在秒杀架构里防止同一用户重复下单这件事不能靠分布式锁分布式锁在超高并发下也是一把性能杀手。我用的最实在的方案就是数据库唯一索引。在订单表或队列表上把user_id activity_id建一个联合唯一索引。同一个用户对同一个活动提交第二次CREATE时数据库会因为唯一键冲突直接报错根本不用在业务代码里做任何判断。一次INSERT失败的成本远低于一次分布式锁获取的成本。5.3 前端防抖与验证码被低估的流量削减器不要小看前端这一层。秒杀按钮在点击之后必须置灰5秒内不允许再次点击活动页面上放置滑块验证或点选验证码虽然会被部分脚本绕过但依然可以卡掉大量初级脚本。还有一个极其有效的做法在活动开始前30秒让前端每秒向服务器发送一次时间同步请求服务器以自己时间为准控制前端按钮在精确时刻变亮。这么做不仅让用户在体验上更公平还能把请求的到达时间波动控制在一个很窄的窗口内服务端可以提前把缓存、线程池、连接池都准备好了。6. 超高并发下的稳定性保障压测、熔断、降级和可观测架构设计得再好没有经过严密的压测验证上线就是一场豪赌。100万QPS这种量级任何环节的短板都会在那一秒内暴露无遗。6.1 全链路压测用更真实的方式制造100万请求传统的JMeter单机压测在超过10万并发时会受限于施压机的千兆网卡根本压不出100万QPS的效果。我使用的方案是分布式压测集群用几十台施压机每台机器跑1-3万并发通过控制台统一编排把所有施压机的请求汇聚到目标系统。压测的目的不是看系统能不能撑住而是找到圧测拐点。比如网关层在多少QPS时CPU软中断开始飙升某个Redis分片在多少OPS时开始出现slow log业务容器在多少QPS时GC时间占比超过5%把每个环节的崩溃前夜状态记录下来就是线上运维时的报警阈值。6.2 熔断与降级策略的预设线上故障是常态预案才是安全网。我的秒杀系统里预设了三层降级降级级别触发条件动作L1某Redis分片不可用本地缓存完全放行用数据库悲观锁兜底概率极小L2订单落库积压严重关闭部分非核心风控规则降低风控拦截率L3系统整体过载网关层直接随机拒绝30%请求确保核心链路逃生说到这熔断和降级的判断依据不是靠拍脑袋而是靠第四点——可观测性。6.3 一分钟以内的故障定位能力做高并发架构最怕的就是出问题后一头雾水。我的做法是给每一个关键服务、中间件、接口埋上四个维度的指标QPS、RTP99、错误率、饱和度。通过Prometheus Grafana统一展示。秒杀活动期间监控大屏上依次盯这几个信号网关层的P99延迟如果超过50ms多半是后端线程池被拖垮了。Redis的blocked_clients如果出现持续上涨说明大量客户端在等待IO可能有大Key。队列表积压量与消费速度的差值如果差值持续扩大说明消费者线程数不够或SQL出现了慢查询。这些信号之间存在因果关系排查线上问题不是到处翻日志而是根据指标链路从上到下、从入口到出口逐层排查。7. 百万QPS背后的工程哲学取舍、防御与敬畏说实话100万QPS的数字再炫酷落到工程上就是一个个务实的取舍。Redis扣减库存的Lua脚本、本地缓存兜底、队列表攒批刷库这些方案单拎出来都不算黑科技但组合在一起它们就能在极端流量下形成一道坚不可摧的防线。我做高并发最大的一次认知转变是终于明白处理流量不是本事提前消灭流量才是本事。整个秒杀架构的精髓从来不在某个中间件用得多好而在于你在一层又一层的链路中到底替下游挡掉了多少本不需要它们去处理的请求。还有一件事想提醒大家千万不要为了追求100万QPS而去高配低用。如果你的业务峰值只有2万QPS那用一台好点的物理机跑Nginx直连MySQL可能就够了。架构的复杂度不是越多越好而是在够用的基础上留出足够的逃生通道。在这个行业里待久了你会越来越敬畏流量峰值。它像一面放大镜把你代码里所有被忽视的角落都照得清清楚楚。把基础功练扎实、把预案做充分剩下的就是在那零点几秒钟里等着风暴来临然后平静地喝一口水了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑