
做过电商后端的人都有体会平时接口一两千QPS大家相安无事一到晚高峰、促销节点流量直接翻几十倍系统开始闹脾气——接口超时、缓存击穿、数据库连接被打满、发券任务堆积。如果你曾在凌晨三点盯着监控面板排查雪崩根因应该能明白“高并发架构”这几个字背后全是实打实的血泪。这篇博文我想聊的是应对晚高峰“几十万并发”的整套系统设计路径。注意几十万并发通常不是指几十万用户同时点击而是指核心入口的QPS每秒请求数达到这个量级。比如晚高峰用户刷首页、加购物车、下单、支付回调叠加秒杀活动时网关层每秒收到的请求可能轻松超过几十万。这不是靠加两台机器就能解决的事它需要从接入层、业务层、数据层、可观测性四个维度做全链路设计。本文适合正在做电商平台、电商SaaS、或任何读多写少的高流量业务的工程师阅读你可以直接拿里面的方案参照落地。1. 先想清楚几十万并发到底意味着什么1.1 并发量级拆解几十万QPS对应的业务场景很多刚接触高并发的同学容易把“百万并发”挂在嘴边但真正做过电商系统的人都知道几十万QPS已经是相当有压力的量级。我们先拆解一下如果网关峰值QPS是50万平均每个请求后端处理耗时50ms那么系统任意时刻的“在途请求”大约是50万乘以0.05秒也就是25000个请求同时挂在系统里。按单台应用服务器能支撑300个并发连接来算至少需要80多台应用实例。注意这只是常规情况如果某个接口RT涨到200ms在途请求会飙到10万需要的实例数量也要翻倍。所以在做架构设计之前第一个要明确的指标是“目标QPS 目标RT”二者缺一不可。再说业务场景。晚高峰的几十万并发和秒杀的几十万并发性质完全不同。晚高峰流量是相对分散的用户访问首页、搜索商品、查看详情、加购、下单路径各异但存在明显的热点数据——比如首页推荐位上的爆款商品、今日主推的会场页面。而秒杀场景是高度集中的所有用户都在请求同一个商品详情、同一个库存接口热点集中在极少数数据上。这两种场景对缓存策略、限流策略、库存扣减方案的要求截然不同不能拿一套秒杀方案去抗晚高峰流量否则你会在缓存上浪费大量内存在限流时误伤正常用户。还有一个容易被忽略的点几十万QPS里可能混杂着大量自动化脚本、爬虫和恶意刷接口的流量。实际工作中晚高峰裸流量看着有30万QPS清洗掉无效流量后可能只剩25万。从架构设计第一天就要把风控和流量清洗考虑进去不然后面做容量评估永远都是虚高的。1.2 高并发系统的核心矛盾与设计原则高并发系统设计本质上是在处理三对矛盾一致性 vs 可用性、延迟 vs 吞吐、标准化 vs 极致优化。电商场景里最典型的是库存扣减。为了保证不超卖最安全的方式是每次扣减都走数据库事务但这个操作在几十万并发下会把数据库打死。于是大家引入Redis扣减库存、异步对账用最终一致性换取高吞吐。“先保证可用再异步保证最终正确”这几乎成了电商高并发场景的默认基调。但你得清楚自己牺牲了什么如果异步对账失败就会产生超卖或少卖的事故所以必须配套对账补偿机制这在我后面第4部分会详细讲。延迟与吞吐的矛盾体现在单个请求处理越慢系统能支撑的吞吐就越低。所以高并发设计里有个原则叫“快慢分离”——快的路径和慢的路径拆开比如下单主流程要轻、要快而发票开具、积分发放、消息通知这些慢操作全部异步化不能拖累主流程的RT。我见过不少团队把发票服务通过Feign同步调用嵌在下单接口里结果对方接口抖动一下下单成功率立刻掉了十个百分点这就是典型的没做快慢分离。标准化 vs 极致优化这个矛盾比较微妙。很多团队喜欢为特定热点场景做定制优化比如把商品详情页直接渲染成静态HTML扔到CDN这确实能扛住巨大流量但维护成本极高。我个人的经验是八二原则80%的流量用标准方案缓存微服务分库分表来扛剩下20%的超热场景再做专项优化。不然系统会变得非常脆弱每上线一个需求都要触碰那些“黑科技”代码开发和运维都痛苦不堪。2. 入口层接入与流量治理2.1 接入层架构从DNS到网关的分层接力入口层是整个高并发系统的第一道防线也是很多人容易忽视的地方。常规的电商接入链路是DNS - CDN - 云SLB/LVS - Nginx - API网关 - 业务应用服务。先说DNS它是第一级负载均衡能做到的是地域级别的流量调度。比如华东的流量进入华东的机房华南的进入华南的机房核心作用是减少跨地域网络延迟同时也能在某个机房故障时把流量切走。对于几十万QPS的体量DNS的TTL最好设置在30秒到60秒之间既保证故障切换速度又避免太快导致DNS解析压力过大。往下一层是负载均衡这里要分清楚四层和七层的差别。LVS属于四层负载均衡工作在内核态性能极高单机可以支撑几十万并发连接适合扛入口的原始流量Nginx属于七层负载均衡能识别HTTP协议做路径转发、URL重写、健康检查。实际部署中通常用LVS做第一级流量入口后面挂多台NginxNginx再转发到业务网关。API网关是我们日常开发接触最多的一层。它承担的不只是路由转发还包括鉴权、参数校验、灰度发布、限流和日志采集。我强烈建议在这一层做全局限流和接口维度的分级限流比如对于下单接口可以针对单个用户设置每秒最多10次请求对于查询商品详情的接口可以设置每秒最多1000次。网关的限流阈值要基于后端实际容量反推而不是拍脑袋定一个整数。这里我要强调一个很多人踩过的坑不要把业务逻辑放进网关层。有些团队为了让网关“更智能”把用户会话查询、购物车合并这类逻辑都塞进网关结果网关变成了单体应用一旦上线新功能就要全量发布风险极大。网关应该保持薄和快只做通用横切逻辑。2.2 流量调度与全局限流降级策略流量调度不只是“把请求分发到不同的机器”更重要的是在异常情况下的流量控制。常用的手段有三类限流、降级、熔断。限流的首要学问是选对算法和维度。我实测下来电商高并发场景最实用的是令牌桶算法因为它允许一定程度的突发流量比较贴合用户行为——比如晚高峰瞬间涌入的点击高峰如果用固定窗口限流很容易一刀切误伤用户。令牌桶的经典实现里你可以设置一个桶容量比如1000和每秒填充速率比如500这样短时间冲到1200个请求也能先放进来但持续超过500的请求会被拒绝。限流维度方面至少要做两层一层是网关层给整个API设置总阈值保护后端整体容量另一层是业务层给热点接口和单个用户设置配额防止某个接口异常拖垮全局。举个例子去年我做一个促销活动时优惠券领取接口突然被脚本刷爆流量直接占了网关总流量的八成。如果没有在网关层做接口级别的优先级区分——把下单、支付这类高优接口单独配额把领券这种低优接口限制在总流量的20%以内——订单接口大概率会被拖垮。降级则是被动的保护手段当依赖的下游服务异常或容量不足时主动选择返回降级内容。电商里最常见的降级是商品详情页的“数据兜底”Redis里的商品信息过期了可以容忍展示旧数据总比用户看到白屏强。所以很多详情页接口会做二级缓存本地缓存RedisRedis挂了就临时读数据库数据库扛不住就返回本地缓存里的旧数据一层一层往下退。熔断机制很多人以为和降级是一回事其实有细微差别。熔断更侧重保护自身不被打垮当下游服务连续错误率达到阈值比如5秒内错误率超过50%熔断器打开直接短路请求不调用下游避免本服务跟着崩溃。我个人建议使用Sentinel或Resilience4j这类成熟组件不要自己造轮子自己对线程池状态判断很容易出边界问题。2.3 热点数据识别与本地缓存在晚高峰场景里流量分布极度不均可能80%的请求都打在20%的商品上这些商品就是热点数据。如果所有请求都穿透到Redis即使Redis能扛住网络带宽和序列化开销也会成为瓶颈。所以热点识别和本地缓存是高并发电商系统的一个关键优化点。热点识别有两条路离线分析和在线统计。离线分析比较容易理解就是根据历史数据提前找出热门商品和热门品类比如运营活动的主推款、直播间上架的商品在活动开始前就把这些商品ID列表同步到各应用节点的本地缓存。在线统计则是在网关或业务应用里实时记录被频繁访问的Key比如在Sentinel的统计逻辑里设置一个规则某个商品详情Key在1秒内被访问超过5000次就自动把它拉入热点名单然后通知各应用节点把这份数据缓存到进程内。这里需要特别说明本地缓存是个双刃剑。它确实能显著降低Redis压力——一个应用节点50个并发查询本地HashMap100个节点只需要承受50×100次本地查找完全不碰网络。但它的致命弱点是数据一致性本地缓存更新滞后用户看到的可能是几秒甚至几分钟前的旧数据。电商场景里对“价格”这种敏感字段做本地缓存要极度小心我见过一个团队把优惠价缓存到本地5分钟结果活动改价后用户端一直显示老价格客诉直接爆掉。稳妥做法是本地缓存只兜底非关键字段或者把本地缓存时间压得非常短比如1-3秒同时配合缓存版本号或消息通知机制去主动失效。3. 业务层微服务拆分与无状态化3.1 微服务拆分原则从“一刀切”到“按域拆分”早些年微服务特别流行的时候流行过“一个接口一个服务”的极端做法结果服务数量爆炸运维苦不堪言调用链拉得极长一次用户请求要串起七八个服务。后来大家慢慢回归理性形成了比较共识的拆分原则按业务域拆分 按读写特征拆分。按业务域拆分比较好理解就是高内聚低耦合把商品、库存、订单、用户、营销、支付这些核心域拆成独立服务。难点在于按读写特征拆分同一个业务域内读操作和写操作的特征差异很大。比如订单服务里创建订单是写多、低频但关键的操作订单查询是读多、高频的操作。如果不拆分一次大促期间查询流量可能把订单写服务拖垮反过来影响下单。所以很多电商团队会把订单服务拆成订单写服务和订单查询服务底层共享同一份数据但查询服务可以走独立的读副本或者搜索引擎互不干扰。拆分的时候还要注意一个原则不要让服务之间的依赖形成环。比如用户服务调用了营销服务营销服务又调用了用户服务两个服务互相依赖一旦其中一个出问题就会双向拖累。我建议在设计初期画清楚服务依赖图发现环就通过引入消息队列或者调整接口归属来打破。判断拆分是否合理的核心标准是能不能做到一个需求只改一个服务如果改一次商品价格要同时动商品服务、营销服务、搜索服务、购物车服务说明拆分粒度有问题。3.2 无状态化设计Session、配置与本地缓存的取舍高并发系统扩容的前提是应用无状态。什么叫无状态通俗说就是任何一个请求发给哪台机器处理结果都一致服务器不保存与下次请求相关的业务数据。最容易犯的无状态化错误是Session放本地内存。传统单体应用用HttpSession存用户登录状态没问题但微服务化后用户请求可能被负载均衡分发到任意一台机器如果Session存在A机器上下一次请求到了B机器用户就掉线了。解决方案很简单Session数据放到Redis或者直接改用JWT这类无状态令牌让请求自带身份信息。这个改造本身不复杂但要注意存量系统的兼容成本——我见过一个老系统改无状态化时忽略了WebSocket长连接里的Session引用整改了整整一周。配置也要外部化。应用里的配置项比如开关、阈值、降级策略不应该写在本地配置文件里硬编码而是放到Nacos或Apollo这类配置中心改配置不用发版秒级生效。晚高峰时可以通过动态调整配置来应急——比如快速把某个非核心功能的开关关掉不必重新发布代码。还有一类容易被忽视的状态是数据库里的“本地临时表”和“应用内的定时任务”。如果每台应用节点都会定时跑Job那就需要引入分布式调度中心比如XXL-Job、ElasticJob保证同一个任务只在一台机器上执行不然会造成重复处理。这个细节在扩容场景里特别坑——系统平时2台机器没事紧急扩容到20台后定时任务被20台机器同时执行产生大量脏数据属于高并发下典型的“扩容事故”。3.3 异步化改造削峰填谷的三种常用手段高并发系统的核心瓶颈往往是那些“必须马上干的事”太多导致的。异步化的本质是把非关键的、时间不敏感的操作从主链路里拆出去削峰填谷。我总结搬家常用的三种异步手段第一种是消息队列比如RocketMQ、Kafka。适用场景是“上游发出事件下游消费处理”。典型例子用户下单成功后订单服务发送一条“订单创建成功”消息积分服务、优惠券服务、消息推送服务各自订阅并处理。这个改造要注意的是消息的可靠性——不能因为异步就丢消息。生产上我习惯把关键消息的可靠性等级调到同步刷盘主从复制虽然会损失一部分吞吐但订单场景宁可慢一点不能丢。第二种是线程池异步化适用于同一个服务内不想阻塞主线程的操作。比如下单接口里要发一封验证邮件没有必要同步等待SMTP返回直接丢给线程池处理。这里最坑的是线程池参数和拒绝策略。我当时在一个订单服务里用默认的DiscardPolicy拒绝策略流量一冲线程池满了直接静默丢弃任务导致大量验证邮件失踪。后来全改成CallerRunsPolicy——线程池满就退回主线程执行虽然主线程会多花一些时间但至少任务不会丢。第三种是请求合并适用于下游支撑能力弱、但又不能丢请求的场景。比如用户批量查询商品状态接口层可以先把请求攒100毫秒然后一次性合并查询数据库把10个独立查询合并成1个IN查询。请求合并能显著降低数据库压力但要注意合并粒度——不能无脑把所有请求都合并否则单个请求延迟会变大。我一般只对纯查询、对延迟不太敏感的接口做合并。4. 数据层缓存、分库分表与最终一致性4.1 Redis高可用架构与缓存设计实战数据层是电商高并发系统最后也是最容易崩的一层。很多团队入口层做得漂亮一到缓存和数据库直接原形毕露。先讲Redis架构。单机Redis无论性能多好单点故障都会要命。生产环境至少要做主从复制哨兵或者直接上Redis Cluster集群。我之前在的一个电商团队用的是Codis方案在7000的QPS下表现稳定但后来流量涨到几万QPSCodis的代理层成了瓶颈最终换成了Redis Cluster。一个经验如果对水平扩展要求高就上Cluster如果只是高可用需求主从加哨兵足够毕竟集群的Key迁移、Slot重分配对运维要求更高。缓存使用有几个高频大坑我一个个说。缓存穿透查询一个根本不存在的数据比如用户查一个被删除的商品ID请求每次都打到数据库。解决方案对空值也缓存或者用布隆过滤器挡掉肯定不存在的数据。我倾向于对“热点查询但冷门数据多”的场景使用布隆过滤器因为它不会把大量空值缓存占用内存。缓存击穿某个热点Key过期的一瞬间大量请求同时打到数据库。解决方式互斥锁重建缓存或者用逻辑过期——缓存数据里带一个逻辑过期时间发现逻辑过期后只有一个线程去重建缓存其他线程继续返回旧数据。缓存雪崩大量Key在同一时间过期数据库被打垮。解决方式给缓存过期时间加随机值比如基础过期时间3分钟再叠加0到60秒随机值让过期时间错开。这里补充一个我自己常用的缓存更新策略先更新数据库再删除缓存。很多同学习惯先删缓存再更新数据库但这样做如果数据库更新失败缓存里一直是旧值数据不一致会持续很久。先更新数据库成功后再删除缓存下次查询时会回填新值不一致的时间窗口非常短。如果你用Canal订阅MySQL的binlog来做缓存更新也是同样的思路——监听数据变更事件异步失效缓存避免业务代码里手写缓存更新的尴尬。4.2 分库分表与读写分离的选型和落地当单库单表的数据量超过几千万或者写入并发达到几千时就该考虑分库分表了。电商最常见的拆分维度是订单按用户ID取模分库分表保证同一个用户的订单都在同一张表里查询用户订单时只用路由到一张表。另一个拆分维度是商家ID适合商家侧查询订单的场景。实际架构中往往要维护两份数据用户维度一份商家维度一份中间通过数据同步保证一致性。分库分表最痛苦的是跨库查询和跨表聚合比如运营后台要查“昨天全平台卖了多少单”按用户维度分表后就需要遍历所有表汇总。我的建议是不要把分库分表的表直接提供给运营查询而是把数据同步到Elasticsearch或ClickHouse里做分析查询。设计分库分表方案时一定要规划好“数据路由规则”和“数据同步链路”这两样做不好后面每逢大促查数据都是一场灾难。读写分离要分场景看。对订单这类写多读多但一致性要求高的数据读写分离需要注意主从延迟。用户下单后立刻查自己的订单列表如果读的是从库而主从同步还没完成用户会看不到刚下的单体验非常奇怪。我的处理办法是订单列表查询走从库但“下单后页面跳转”的前几次查询强制走主库或者设计一个“近期订单查询优先读主库”的规则等主从延迟时间过后再全部走从库。4.3 秒杀与抢购场景的库存扣减方案库存扣减是高并发电商系统里技术含量最高的一环稍有不慎就是超卖事故直接经济亏损。先说不推荐的方案直接在数据库里update stock stock - 1 where id ? and stock 0。这个方案在低并发下没问题但几十万并发下数据库的行锁竞争会直接拖垮数据库性能极差。业界比较成熟的方案是Redis Lua脚本扣减库存然后异步同步到数据库。具体做法是用Redis的Hash结构存储商品库存扣减时执行一段Lua脚本先判断当前库存是否大于0然后扣减并返回新库存。Lua脚本在Redis中是原子执行的不会出现并发超减。核心逻辑是local stock tonumber(redis.call(hget, KEYS[1], stock)) if stock tonumber(ARGV[1]) then redis.call(hincrby, KEYS[1], stock, -tonumber(ARGV[1])) return 1 else return 0 end注意这里的关键是“预扣”和“实扣”分离。用户点击下单时先Redis预扣库存然后创建本地订单订单状态为待支付支付成功后异步把Redis扣减的库存同步到数据库做扣减如果用户支付超时取消订单需要把已扣的库存回补回去。这套方案需要非常谨慎地处理库存回补的幂等性不能让一个取消请求回补两次。还有一个必须做的是限购。真正的秒杀活动往往有严格的数量限制——每人限购一件。限购的判断可以在Redis里用Set记录已购买用户ID或者用布隆过滤器做非精确判重。我之前吃过一次亏只做了库存扣减没做限购结果单个黄牛账号用脚本抢了上百件商品活动运营直接崩溃。从那以后我强烈建议任何秒杀活动都必须有“用户维度的购买频率校验”而且这个校验要放在网关层先执行一道再在业务层执行一道双保险。5. 可观测性与故障应急5.1 全链路追踪与核心指标监控系统越复杂定位问题的难度越大。一个下单请求可能经过网关、订单服务、库存服务、支付服务晚高峰出问题时没有一套全链路追踪系统几乎无从下手。这里最成熟的方案是OpenTelemetry 类似SkyWalking或Jaeger的链路追踪平台。核心思路是在请求入口生成一个TraceId然后透传到所有下游调用中把整条链路的耗时、错误、日志串起来。接入的时候记得要对HTTP请求头、RPC调用、消息队列的消息头都做拦截透传特别是通过MQ异步处理的任务很多人漏了MQ透传导致链路断掉排查问题直接被隔断。监控指标分三种系统指标CPU、内存、磁盘、网络、应用指标QPS、RT、错误率、线程池活跃数、业务指标订单创建量、支付成功率、购物车加购次数。我在电商团队里最关注的业务指标其实是“订单创建到支付成功的转化率”因为它的波动最能反映用户体验和系统健康程度。晚高峰监控大屏上如果订单创建量正常、但支付转化率突降基本可以判断是支付回调链路出了问题而不是入口流量异常。日志这块也要讲究。高并发场景下不能每行日志都打全字段否则日志系统本身会成为瓶颈。我习惯用结构化日志把核心字段抽取到log字段里比如用户ID、商品ID、traceId、耗时便于检索同时把大文本内容比如请求Body做采样记录只保留一定比例的错误请求详情。5.2 大促前的容量评估与压测每次晚高峰大促前团队都应该做一次容量评估和压测而不是到了那天硬抗。容量评估的公式不复杂所需实例数 预估峰值QPS × 单请求平均RT / 单实例最大并发处理能力举个例子预估峰值QPS 30万接口平均RT 50ms那么系统的在途并发是15000。如果单台应用实例能承受500个并发取决于线程池大小、CPU核数那理论上需要30台实例。但千万不要按这个理论值直接部署因为RT不是恒定50ms流量一旦超过某个阈值RT会指数级上升导致在途请求暴涨、系统崩溃。所以压测的目的就是找出那个“拐点”——在哪个QPS下RT开始明显劣化用拐点前的容量来定余量。压测工具推荐用wrk和JMeter结合wrk做纯接口性能测试压出单机极限值JMeter做全链路场景测试模拟晚高峰用户行为路径浏览首页→搜商品→看详情→加购→下单。全链路压测要注意清理测试数据不要让压测产生的脏数据污染库存和订单。比较稳妥的方式是在压测环境使用独立的影子表通过拦截压测流量标识把数据路由到影子表避免影响真实业务数据。5.3 故障预案与降级演练宁可备而不用高并发系统的稳定性不在于架构方案多华丽而在于故障发生时是否有预案并能快速执行。我到一个新团队的第一件事就是梳理一份核心链路的故障预案清单至少包含以下场景数据库主库宕机确认从库数据延迟决定是否需要手动切换切换后的写流量如何兜底。Redis集群不可用缓存降级后数据库能扛住多大流量是否需要立即触发限流。下单服务大面积超时是要熔断商品详情查询还是熔断优惠券计算还是直接开启“单商品快速下单模式”。消息队列堆积是扩容消费者还是暂时关闭非核心消费者保证核心消息优先处理。预案写了不演练等于白写。每隔一到两个月团队就应该在预发环境做一次故障演练人为杀掉一个核心节点验证监控告警是否准确、预案操作是否顺手、恢复流程是否清晰。我第一次组织这种演练时发现团队里竟然没人知道主从切换的脚本放在哪台机器上这就很说明问题。经过几轮演练后故障恢复时间从最初的三四十分钟压缩到五六分钟这才是预案真正的价值。6. 关于“几十万并发”的一句大实话网上聊高并发架构的文章很多但落到实际每个电商团队面临的流量特征、业务复杂度、团队规模都不同照搬别人的方案往往会水土不服。我自己踩过最大的坑是最初做高并发设计时总想把架构一步到位——什么分布式事务、单元化部署、自研网关全都想用上。后来发现系统复杂度增加的代价远超流量增长带来的收益。现在我更相信一个原则架构是长出来的不是设计出来的。先从简单的方案开始用监控数据判断瓶颈在哪里再针对性地做局部升级。比如入口压力大就先做本地缓存和限流数据库读压力大就上读写分离和分库分表链路追踪缺了就补全链路监控。每一步都基于真实压测和监控数据来做决策。另外想提醒一句高并发系统最怕的不是流量大而是人员的“我以为系统能扛住”。成熟的团队会坚持做容量评估、压测、故障演练把这些当作例行工作而不是大促前才加班临时抱佛脚。几十万并发不是一两个亮点技术能解决的它考验的是从接入层到数据层的每一环是否扎实以及出了问题之后团队能不能快速定位并止血。最后分享一个实用小技巧设计任何高并发预案的时候先把“最坏情况下用户会看到什么”想清楚。如果流量实在扛不住你希望用户看到的是“商品已售罄”还是“服务繁忙请稍后再试”把这个预期先和产品、运营对齐技术上的降级策略才能做对。这是我在几次凌晨故障中总结出来的——做技术决策时用户感知永远是最终的检验标准。