1. 双11流量怪物的真实面貌:到底要扛多高的并发
先说一个很多人对高并发的误解。不少人觉得,高并发就是“用户多、请求多”,系统加两台机器、配个负载均衡就能搞定。但真到了双11这种量级,问题完全变了性质——它不只是“量”的问题,而是“瞬时冲击”和“系统性连锁反应”的问题。
拿我自己做过的电商活动系统来说,日常流量峰值QPS可能在几千到几万,但大促一开场,流量能在几秒内暴涨几十倍甚至上百倍。这个涨法不是平滑曲线,而是一根接近垂直的陡线。如果架构在10秒内扛不住这个陡线,后面就是雪崩:数据库连接被打满、应用线程池阻塞、依赖服务相互拖垮、日志系统被冲爆,最终整个链路瘫痪。
双11和普通高并发场景有几个本质区别,理解这些区别,才能明白为什么架构要那样设计:
第一,峰值极其集中。 预售、开售都有固定时间点,流量在那一瞬间集中涌入。2019年双11的订单创建峰值达到54.4万笔/秒,这个数字背后对应的系统QPS远不止百万。普通业务不会遇到这种瞬时脉冲,所以常规的“扩容+限流”思路在这里不够用,必须在架构层面就具备“削峰填谷”的能力。
第二,写多于读且强一致。 电商大促的核心动作是下单、扣库存、支付,这些全是写操作,而且涉及资金和库存数量,不能像读场景那样随便加缓存。你可以在商品详情页扛住百万QPS,但订单创建和库存扣减这两个环节,每一笔都必须精确落库,每秒几十万的精确写操作,这对存储层的压力完全不在一个维度。
第三,依赖链路极长。 用户一次下单,背后涉及商品中心、库存中心、订单中心、支付网关、营销系统、用户中心、物流系统等十几个系统的协同调用,每个系统的延迟会累加,任何一个环节抖动都可能放大成整体故障。
这三个本质区别决定了双11架构的核心不是“买更多机器”,而是四个字:弹性、治理、分层、兜底。
下文我会按一条完整的架构主线来拆解,从流量入口到数据存储,把每个环节的经典设计思路和取舍逻辑讲明白。这不是让你照搬阿里的方案——普通项目也没那个体量——而是让你理解每一个核心决策背后的“为什么”,从而能在自己的系统里做出正确的权衡。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 纵览全局:支撑亿级流量冲击的架构骨架长什么样
2.1 一次请求要经过多少道关卡
我们以用户在大促期间点击“立即购买”为例,走一遍完整链路。这个链路本身就是高并发架构的“骨架”:
DNS解析 + CDN加速 → 全局负载均衡(GSLB)→ 入口网关(API Gateway)→ 接入层集群 → 业务微服务(商品、营销、订单)→ 缓存集群 → 消息队列 → 数据库读写 → 异步对账/清算
每一层存在的意义都不同:
- CDN:挡掉静态资源的重复请求,图片、脚本、页面片段都在离用户最近的节点返回,根本不进源站。
- 入口网关:做统一的鉴权、限流、协议转换、灰度路由,是整个系统的“第一道安检门”。
- 接入层集群:无状态的水平扩展层,所有节点都能处理任意请求,靠负载均衡把流量打散。
- 业务微服务:按域拆分的核心逻辑层,每个服务独立部署、独立扩缩容。
- 缓存:扛住90%以上的读流量,让请求尽量不穿透到数据库。
- 消息队列:把突发的写流量转化为平滑的异步处理流,是削峰的核心武器。
- 数据库:最终的数据一致性底座,所有系统最终都要在这里落账。
这套骨架的关键在于每一层都是可水平扩展的,并且每一层都能独立扩张和收缩。当流量翻倍时,不是整体加机器,而是按瓶颈层单独扩容。
2.2 无状态设计是第一原则
整个链路里最容易被忽视、也最重要的一条铁律是:所有应用层节点必须无状态。
什么叫无状态?就是任何一个请求打到任何一台机器上,处理结果都一样。用户登录状态放在Redis或Token里,而不是存在本地Session里;业务数据不落本地磁盘,全部走分布式存储。只有这样,系统才能做到随时加减机器、随时重启、随时故障转移。
有状态的服务是高并发架构的噩梦。比如你把用户购物车数据存在应用服务器的本地内存里,这台机器一挂,所有在这台机器上的用户购物车全丢,而且你想扩容都扩不了——新加一台机器,数据不在这台机器上。
阿里在早期也踩过这个坑,后来花了大力气做“应用无状态化改造”。现在你去面试,如果连“无状态”这个原则都答不到,基本第一轮就被刷了。所以记住这条:能放外部的数据绝不放本地,能在请求中传递的状态绝不落服务器。
2.3 单元化与多地域部署:把流量切割成可管理的小块
到了阿里这个体量,单地域部署已经无法满足容灾和资源需求。他们的做法是“单元化”架构——把整个系统按地域和数据中心拆成多个“单元”,每个单元是一个完整的、可独立运行的全栈系统副本,用户流量按规则(通常是用户ID哈希)路由到某个单元内闭环处理。
这样做有三个直接好处:一是流量被切分成小块,每个单元只承担一部分,单单元的并发压力可控;二是故障隔离,一个单元出问题不会拖垮全局;三是不用大规模跨地域调用,减少网络延迟。
普通项目不需要做到单元化,但这个思路值得借鉴——把系统按某种维度(用户维度、地域维度、业务维度)切分成互不干扰的独立区块,每个区块都有自己的流量上限和容灾能力。比如你的系统有多个核心租户,完全可以在架构上让每个租户独立部署一套资源池,避免相互影响。
2.4 弹性伸缩:按需分配,而不是常年撑着峰值
双11时期的流量是日常的几十倍,如果常年按峰值容量准备机器,那是在烧钱养闲置资源。阿里的做法是容器化+Kubernetes集群,配合HPA(Horizontal Pod Autoscaler)和自定义指标实现自动扩缩容。
具体落地时,扩容策略有两个关键决策点:
- 扩容的触发条件:除了CPU、内存这类基础指标,更要关注“排队长度”和“P99延迟”。CPU还没涨,但请求已经在排队了,说明后端瓶颈,该扩的可能是下层。
- 扩容的提前量:大促流量是有预告的,不能等流量到了再扩,要在流量预期到达前提前扩容到位。这里需要运营数据和容量评估,比如“预热扩容”模式,提前2-4小时把集群扩到目标规模,流量到达时正好承接。
我之前做过一个经验总结:自动扩容的指标至少要提前一个监控周期发现异常,否则扩容永远追不上流量暴涨的速度。 所以实际工程里往往混合使用“定时扩容”(根据大促节奏预判)和“指标扩容”(实时响应异常)。
3. 流量治理:高峰期如何给系统做“交通管制”
3.1 限流:不是拒绝用户,而是保护系统整体
一说限流,很多人的第一反应是“把用户拒之门外”,这个理解是错的。限流不是拒绝,而是“牺牲局部保全局”。当一个系统已经接近处理极限时,继续放流量进来的结果不是多处理几个请求,而是整个系统崩溃,所有请求都处理不了。限流的本质是让系统在能力范围内稳定服务,而不是在压力下彻底死亡。
主流限流算法有四种:固定窗口、滑动窗口、漏桶、令牌桶。它们的核心区别在于“允许的突发程度”不同。
| 算法 | 突发容忍度 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| 固定窗口 | 高(有临界问题) | 低 | 简单按秒限流 |
| 滑动窗口 | 中 | 中 | 需要精确控制窗口 |
| 漏桶 | 无(完全匀速) | 中 | 保护下游系统,输出恒定速率 |
| 令牌桶 | 有(允许短时突发) | 中 | 接口调用、API网关 |
大促场景下用得最多的是令牌桶及其变体,因为它既能限制平均速率,又允许短时间的突发流量,符合电商活动的特征——开售瞬间用户蜂拥而入,但系统能承受短暂的超载。
我用Go写过一个简单的令牌桶限流器,核心逻辑就几十行,但理解它比背代码重要:
go复制type TokenBucket struct {
rate float64 // 令牌产生速率(个/秒)
capacity float64 // 桶容量,即最大允许的突发量
tokens float64 // 当前令牌数
lastTime time.Time
mu sync.Mutex
}
func (b *TokenBucket) Allow() bool {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
// 按时间补令牌:距离上次请求过去了多少秒,就补充多少令牌
b.tokens = math.Min(b.capacity, b.tokens+now.Sub(b.lastTime).Seconds()*b.rate)
b.lastTime = now
if b.tokens < 1 {
return false
}
b.tokens--
return true
}
注意一个细节:令牌桶的“突发”能力来自capacity参数。同样设置rate=1000,capacity=100时只允许短时100个突发,capacity=10000时就能承受瞬间一万个请求。这个参数要根据后端能力实测决定,不是拍脑袋定的。
3.2 熔断:别让一次故障变成系统性雪崩
限流管的是“流量进来太多”,熔断管的是“下游已经不行了”。假设你的订单服务要调用库存服务,库存服务因为某种原因响应变慢了,从原来的5ms变成5秒。如果你的订单服务还在傻等每个库存调用的响应,很快线程池就被占满,新的请求全部排队,最终订单服务自己也挂了——这就是级联故障。
熔断器借鉴了电路熔断的思路,动态维护三种状态:闭合(Closed)、断开(Open)、半开(Half-Open)。
- 闭合:正常运行,请求正常发往下游,统计失败率。
- 断开:失败率超过阈值(比如连续失败5次或错误率超过50%),直接短路,后续请求不再调用下游,而是立即返回兜底结果。
- 半开:断开一段时间后(比如30秒),放一小部分试探流量过去,如果成功则恢复闭合,失败则继续断开。
熔断的核心价值在于快速失败。当依赖不可用时,与其让所有请求阻塞等待超时,不如直接告诉调用方“当前不可用,请走降级逻辑”。与其说是“让系统变得更好”,不如说是“让系统坏得更体面”。
3.3 降级与兜底:保命时只留核心逻辑
双11架构里有个著名的原则:“一切不核心的链路都要能一键关闭。” 你在页面上的推荐位、评价展示、历史订单、积分提示,这些feature在峰值期是可以砍掉的。每个服务在开发时就要设计好降级开关,一旦系统压力过大,通过配置中心下发命令,立即关闭非核心功能,把计算资源让给订单、支付等核心链路。
我在实际项目里做的降级方案分三个级别:
- 一级降级(最轻):关闭非核心的日志上报、埋点采集,减少I/O开销。
- 二级降级(较狠):关闭非核心的关联查询,商品详情页不展示评价、不展示推荐列表,只返回基础信息。
- 三级降级(最狠):直接返回兜底页面,不调用任何后端,只靠CDN上的静态页面维持服务。
降级策略必须配合开关管理系统,并且所有开关要支持“一键全局关闭”。大促期间出问题的时候,没人有时间一个个服务去配开关,必须有一个总控开关能瞬间切换全局状态。
3.4 削峰填谷:用消息队列把突刺拉平
消息队列是双11架构里最常用的削峰工具。它的核心思想很简单:同步调用改成异步化,流量先进队列,由消费者按自己的节奏处理。
举一个库存扣减例子。正常情况下,用户下单后要同步扣减库存,数据库写入压力直接和用户请求量成正比。引入MQ后,下单请求先发一条“扣减库存”消息到队列,立刻返回“下单成功”,后台消费者以固定速率从队列拉取消息执行扣减。
这样做有两个直接效果:
- 系统不再被瞬时流量打穿——队列就像一个蓄水池,洪峰进来先蓄着,下游系统按自己的最大处理能力慢慢消化。
- 下游负载恒定——消费者速率可调,不会出现“流量大时数据库被打爆,流量小时数据库闲置”的情况。
但异步化也带来一个新问题——一致性。你告诉用户“下单成功”,但库存其实还没扣减,万一消费者处理失败怎么办?这就需要配合后面的对账、重试、幂等机制。我后面会专门讲这部分,这里先记住:异步削峰不是把问题变没了,而是把“水平压力”换成了“垂直处理时间”,能不能行得通,取决于你能不能接受结果延迟到达。
3.5 前端与页面优化:流量从源头减少
架构设计不只在后端,前端同样在“治理流量”。大促页面有几个常见的流量优化策略:
- 页面静态化:商品详情页在发布时直接渲染成静态HTML,配合CDN分发,用户请求根本到不了动态服务器。动态数据(价格、库存)通过单独的异步接口按需加载。
- 接口聚合:移动端一次请求聚合多个接口的数据,减少网络往返次数。比如商品详情页一次请求返回所有楼层数据,而不是首屏五个接口。
- 请求合并与防抖:搜索框输入防抖、列表滚动懒加载,这些看似小事,在千万级用户体量下能砍掉大量无效请求。
4. 缓存体系:扛住大多数读流量的关键
4.1 多级缓存架构的每一层各自干什么
高并发系统里,Redis不是缓存的全部,它是一个层。完整的缓存体系从上到下是这样的:
第一层:浏览器缓存与CDN缓存。 静态资源(图片、CSS、JS、部分页面片段)在离用户最近的地方返回。这一层吃掉的是“用户离源站最近的网络链路上的重复流量”。我们自己监控的数据里,CDN能挡住整体流量的70%以上。
第二层:应用本地缓存(L1 Cache)。 每台应用服务器进程内维护一份,用Caffeine或Guava Cache实现。这一层的特点是快得离谱(纳秒级访问),但容量有限,且需要处理进程间缓存不一致问题。适合存那些变化极慢的配置数据(如商品类目树、城市列表)。
第三层:分布式缓存(L2 Cache)。 也就是Redis或Tair集群,这是后端的“缓存主战场”。所有需要跨节点共享的热点数据都存在这一层,比如商品基本信息、库存数量(预扣版本)、用户购物车等。
这三层缓存的访问顺序是:本地缓存没命中 → 查Redis → Redis没命中 → 查数据库。每往下一层,延迟增加一个数量级,但数据新鲜度和容量也增加一个数量级。
4.2 缓存穿透、击穿、雪崩:三个经典问题必须懂
这三个问题是面试必考,也是实操里最容易踩的坑。我直接用大白话解释加方案。
穿透:查询一个根本不存在的数据。 每次请求都绕过缓存直达数据库。如果有人恶意用不存在的ID大量刷接口,数据库瞬间被打满。
解决方案:
- 缓存空值:不管数据是否存在,都缓存一个空结果,设置短过期时间(比如60秒)。
- 布隆过滤器:把所有存在的ID先放到布隆过滤器里,请求进来先过一遍过滤器,不存在直接返回。
击穿:某个热点key突然过期,大量并发请求同时打到数据库。 比如爆款商品的详情key过期,瞬间十几万人同时来查,数据库扛不住。
解决方案:
- 互斥锁(Mutex):发现缓存没有后,先尝试获取分布式锁,只有拿到锁的线程才去查数据库,其他线程等待锁释放后直接读缓存。这在大促场景里非常有效,代价是请求会增加一个锁等待时间。
- 逻辑过期:不给key设真正的过期时间,而是存一个“逻辑过期时间戳”,读取时发现过期,自动触发异步刷新。用户拿到的可能是旧数据,但系统扛住了。
雪崩:大量key在同一时刻过期,或缓存集群整体宕机,所有请求直达数据库。 这是最严重的一种,数据库可能直接被打死。
解决方案:
- 过期时间加随机化:基础过期时间加一个随机值(如5分钟加0-60秒随机数),避免大面积的key同时过期。
- 集群高可用:Redis主从+哨兵/Cluster,保证缓存层本身不挂。
- 缓存层熔断降级:如果缓存层完全不可用,强制开启降级,直接返回兜底数据,而不是穿透到数据库。
我在做缓存时有一个心得:缓存层出问题的破坏力远大于数据库出问题的破坏力,因为缓存出问题时所有请求都直达数据库,相当于把压力放大了数倍。所以缓存层的可用性优先级高于一切。
4.3 缓存与数据库的一致性:先更新谁是个大问题
经典场景:商家改了商品价格,要先更新数据库还是先更新缓存?选错了会读到脏数据。
目前业界并没有“绝对一致”的方案,只有“技术上可接受”的方案。主流做法有两种:
Cache Aside模式(旁路缓存):
- 读:先查缓存,没有则查数据库,回填缓存。
- 写:先更新数据库,再删除缓存(不是更新缓存)。
- 删除失败怎么办?用Binlog订阅(如Canal)异步删缓存,或者等缓存自然过期。
Write Through模式(写穿透): 更新数据库的同时同步更新缓存,由缓存组件保证原子性。实现复杂,但一致性更好。
我实际项目里最常用的是Cache Aside + 延迟双删。所谓“延迟双删”是:先更新数据库,删除缓存;过几百毫秒后再删一次缓存。为什么删两次?因为第一次删除后,可能有一个并发请求刚读了旧数据正在回填缓存,如果这个请求比第二次删除先完成,缓存里就还是旧数据。延迟双删把这个窗口尽量压缩,但仍然不是绝对安全的。
这里有个残酷的现实:只要是缓存+数据库的组合,就做不到强一致。 业务上要做的是根据读多写少程度和数据敏感性决定接受程度。商品价格这种数据,可以接受秒级延迟;但库存和余额,必须尽量走强一致链路。
4.4 热点数据的检测与处理
双11场景里,热点的表现是“几百万人同时看同一个商品”。这个商品如果按普通商品的缓存策略,Redis的单个key会承受巨大的访问量,甚至打爆单个Redis分片。
热点处理思路是热点提前识别 + 打散:
- 提前识别:运营知道哪些商品会上大促主会场,直接把这些商品标记为热点,提前预加载缓存,并做多副本存储。
- 动态识别:集群实时统计每个key的访问频率,超过阈值自动把key复制到多个Redis节点(比如同一个商品的key拆成goods:123456:0、goods:123456:1……goods:123456:9,每个副本分布在不同分片),请求按hash散到不同副本。
- 本地缓存兜底:对超热点商品,直接把数据推到每台应用服务器的本地缓存,避免所有流量都打到Redis。
这套组合拳打下来,热点商品对系统的冲击能降低一个数量级。我见过很多项目一开始就只加Redis,不做热点检测和本地缓存,结果Redis成了新的瓶颈——记住一句话:任何单点组件都可能成为热门流量的瓶颈,包括Redis本身。
5. 订单与库存:数据存储层的核心设计
5.1 分库分表:库拆了之后,复杂度才刚刚开始
流量进来后,最终的压力都要落到数据库上。双11的订单数量是亿级起步的,单表、单库根本存不下也扛不住,分库分表是必由之路。
分库分表两个维度:垂直拆分(按业务域拆,比如订单库、库存库、用户库)和水平拆分(拆成多个结构相同的分片,每片数据量相当)。
水平拆分的核心是选对分片键。订单表最常用的分片键是用户ID或订单ID。我推荐用订单ID或用户ID,因为订单查询场景绝大多数是“按用户查我的订单”和“按订单号查详情”,选这两个键做分片可以让单条查询落在单个分片上,避免跨分片查询。
我实际调研过一些项目拆库后的问题:大部分事故不是拆之前的,而是拆之后的。 跨分片事务、跨分片查询、全局ID、数据迁移,每个都是大坑。
- 跨分片事务:传统数据库事务在分片后失效,需要用分布式事务方案(下面细说)。
- 跨分片查询:如果查询条件不包含分片键,需要广播到所有分片再合并结果。大促场景禁止这种查询,必须强制以分片键维度查询。
- 全局ID:不能再用自增ID,需要全局唯一ID生成器,经典方案是雪花算法(Snowflake),结合时间戳+机器ID+序号生成64位ID。
5.2 库存扣减的四种方案对比
库存扣减是电商高并发场景里最核心、最复杂的问题。它的难点在于:数据量不大,但并发量极高,而且强一致。
方案一:数据库行锁直接扣减
sql复制UPDATE stock SET available = available - 1 WHERE sku_id = ? AND available > 0;
这行SQL用数据库的原子性保证不会超卖。问题是单SKU的更新会形成行锁竞争,几十万并发同时更新同一行时,数据库扛不住。
方案二:乐观锁版本号
先SELECT出version,UPDATE时校验version是否变化,失败则重试。相比方案一减少了锁等待时间,但高并发下大量事务因version冲突而重试,数据库压力反而更大。
方案三:Redis预扣减 + 异步DB同步
先用Redis的原子操作扣减库存,扣成功再发MQ消息异步更新数据库。这是目前大厂的主流方案,它能扛住极高的并发,但存在“Redis扣了、DB没更新成功”的极端情况,需要补偿机制。
方案四:请求串行化
把同一个SKU的所有请求按一致性哈希路由到同一个队列(或同一台机器的本地队列),由单线程依次处理扣减。串行化后不用担心并发问题,扣减变成简单顺序操作。缺点是单SKU的吞吐能力受限于单线程处理速度,但电商场景里单SKU一秒几千笔订单已经足够。
我自己实践中比较推荐的组合是:Redis预扣减做流量承接(方案三)+ MQ异步落库 + 对账任务兜底(消费失败后人工或自动补偿),核心SKU做方案四串行化兜底,双保险。
5.3 分布式事务:从强一致到最终一致
分库分表后,一次下单操作可能涉及订单库、库存库、用户账户库等多个库,本地事务管不了了,必须引入分布式事务方案。
这里先把概念理清。分布式事务有几种模式:
XA强一致事务:两阶段提交(2PC),保证所有参与方同时提交或同时回滚。但性能和可用性差,协调者单点、同步阻塞,大促场景基本不用。
TCC(Try-Confirm-Cancel):每个操作拆成三个动作,Try阶段预留资源,Confirm阶段确认,Cancel阶段回滚。优点是强一致、灵活;缺点是业务侵入大,每个操作都要写三个接口。
本地消息表/事务消息:核心思路是“本地事务+消息发送”绑定在一个事务里。比如订单写入本地库,同时写一条“待发送消息”,然后通过MQ发送,消费者收到后执行下游操作,执行成功后再确认消息。最终一致性靠消息的可靠投放和幂等消费保证。
SAGA模式:把一个长事务拆成多个短事务,每个事务执行后记录状态,失败时执行反向补偿操作。适合跨多个服务的长流程,但不适合强一致场景。
大促场景里选型原则是:能用最终一致解决的绝不强求强一致。 订单创建和库存扣减之间有短暂的不一致窗口(比如订单状态是“待支付”,库存可能还没扣减),用户层面是无感知的——他还没付钱,库存暂时不扣也无所谓。真正需要强一致的是支付成功后的状态流转,这块用TCC或对账补偿来保证。
5.4 幂等与对账:异步化后的最后一道防线
前面说了一堆异步化、MQ削峰,但异步系统的最大敌人是重复消息。网络抖动、消费失败重试、生产者重发,都会导致同一消息被消费多次。如果不做幂等,用户下了一单被扣两次库存、创建两个订单,那就出大事故了。
幂等的核心是唯一键。每个请求在入口生成一个全局唯一ID(如“userId + 时间戳 + 随机数”生成的业务流水号),整个链路上都携带这个ID。数据库表里对这个ID建唯一索引,重复插入时直接冲突失败。MQ消费者在处理消息时,先查询本地库里的订单号是否已存在,存在则直接标记已消费,不重复处理。
对账则是最后兜底。每天凌晨跑对账任务,比对订单系统的数据、库存扣减流水、支付网关的数据是否一致。不一致的记录进人工处理队列。双11期间每天对账的订单量是上亿级别的,对账系统本身也要做得非常高效。 我见过把对账SQL写得很烂导致跑了一整天都没跑完的例子,对账任务的核心策略是“分片+分批”,按时间切段,每段并发处理。
6. 全链路压测与可观测性:怎么在流量到来前发现问题
6.1 全链路压测:在真实环境模拟双11流量
大促最怕的是什么?是“上线前看着好好的,流量一进来就崩”。普通的单元测试、接口测试根本模拟不了真实场景。所以大厂的做法是全链路压测——在准真实环境中,模拟双11的流量模型,把整个系统打一遍。
我听过的全链路压测玩法有两种:
- 测试环境压测:搭建一套和生产等量的环境,用压测工具往里面打流量。问题是要准备等量资源,成本极高;另外测试环境的配置和调优逻辑不一定和生产完全一致,结论可信度打折。
- 生产环境压测:直接在线上做压测,用“影子流量”或“压测标”把压测请求和生产流量混在一起。关键点是压测请求必须能被识别(打上特殊标记),在数据写入时自动路由到影子表/影子库,不污染生产数据。阿里就是用的这种方式,每年双11前在线上做几轮全链路压测,发现瓶颈就扩容调优。
全链路压测最有价值的产出不是“测出最大QPS”,而是提前把系统里的短板暴露出来,然后在真正流量到来前修复。每个环节的承载上限、队列积压恢复能力、数据库连接数的最大可配值,全部在压测阶段摸清。
6.2 链路追踪与监控:出了问题要在5秒内定位
高并发系统规模庞大,一个请求可能经过几十个服务节点。出了问题最怕的就是“不知道问题在哪个环节”。这就是链路追踪系统存在的意义,它把一次请求经过的所有节点的耗时、状态、日志串成一条完整的Trace。
链路追踪的核心是生成和传递一个全局traceId。请求入口生成traceId,通过HTTP Header(或RPC上下文)传递到每一层,每一层把本节点的span(耗时、状态、自定义标签)上报到collector。查问题时,输入traceId就能看到完整调用链路和时间消耗分布。
我自己的监控体系里最看重三个指标:
- QPS和错误率:基础指标,异常时立刻能看到。
- P99延迟:P99是1%的请求比这个值慢,如果P99涨了,说明系统接近瓶颈了。大促时P99涨到平时的2-3倍,基本就是限流降级要开启的信号。
- 队列积压量:MQ消费侧的积压数量直接反映“削峰填谷”消化得如何。积压持续上涨说明消费者处理能力不足,需要加消费者实例或优化消费逻辑。
监控的价值不是“看”,而是自动化响应。大促期间靠人盯监控根本来不及,必须配合告警规则和自动伸缩。常见的组合是:P99延迟超过阈值 → 触发告警 → 自动扩容该服务实例;连续N次心跳失败 → 触发熔断降级 → 保护后端。
7. 给普通项目的启示:双11架构不是只能仰望的“神仙方案”
很多同学看完这类架构解读的第一反应是:“这些方案太复杂了,我们的项目根本用不上。”这个想法我需要纠正一下。双11架构里的很多设计思路,在不同的量级下都有可借鉴的落地方式,关键要做的是一件事:先识别你系统的真正瓶颈在哪里,再选择对应的治理手段。
我把这套架构的核心方法论浓缩成一张“优先级清单”:
- 第一优先级:无状态化和水平扩展。这是高并发架构的地基,不满足这个,其他都免谈。
- 第二优先级:缓存和异步化。用缓存扛读,用MQ削峰填谷,用较低的改造成本换来数量级的性能提升。
- 第三优先级:限流、熔断、降级。这三件套保证系统在遭遇突发流量时不会崩溃。
- 第四优先级:分库分表和分布式事务。只有前面都做完了还是不够用,才走这一步,因为它的业务侵入最大、成本最高。
- 第五优先级:全链路压测和可观测性。这是保障,确保前四步在任何时候都处于可控状态。
从面试角度看,如果你能把这套体系讲清楚,并且每个环节都能说出“为什么这样设计”“能解决什么问题”“存在什么代价”,那就已经超过大多数候选人。面试官真正想考察的不是你是否背过Redis的八股,而是你能否在真实场景中做出正确的技术决策。
最后分享一个我个人在大促期间固定要做的一项清单:checklist不是上线前一天才过,而是提前两周就开始逐项确认。 压测数据是否达标、限流阈值是否需要调整、监控告警是否覆盖所有核心链路、所有降级开关是否能在5分钟内一键生效、对账任务是否能按时跑完——这些事看着琐碎,但每一次大促事故几乎都能回溯到某个“没提前准备”的环节。高并发架构的真功夫,说到底不是某个精妙的算法,而是把每个可能出问题的环节想清楚,并且用机制去兜底。
