前两年做年中大促保障的时候,我们技术群里凌晨三点还在刷屏,核心交易接口的QPS从平时的几百一路飙到几万,监控面板上的红线一直在跳,那会儿我才真正意识到:高并发处理不是一个性能优化话题,而是一个系统设计的核心命题。后来陆陆续续帮朋友公司排查过几次线上事故,发现很多人对高并发的理解还停留在“加机器、调线程池”这个层面,结果一到业务抗峰的时候,系统就像被大水冲过的堤坝,到处都在漏水。
这篇文章我想把“高并发处理”这件事掰开揉碎了讲清楚。它到底在解决什么问题,衡量指标有哪些,业务抗峰要具备哪些能力,以及这些能力在实际落地的时候有哪些坑。不管你是刚接触后端开发的新人,还是已经在负责核心系统的架构师,这篇文章应该都能提供一些值得借鉴的思路。
1. 高并发处理到底在解决什么问题
1.1 高并发不是“快”,而是“稳”
很多人有个误区,觉得高并发处理就是让系统响应速度变快。其实高并发追求的核心指标是“稳定性”,不是“速度”。举一个很直白的例子:平时某接口只需要50毫秒就能返回结果,但当流量突然变成平时的50倍时,接口的响应时间可能从50毫秒劣化到5秒,这时候系统不是变慢了,而是接近崩溃了。
高并发场景的本质矛盾是:有限的系统资源,面对的是突发的、海量的请求压力。CPU有核数上限,内存有容量上限,数据库连接池有最大连接数,带宽有出口上限。当请求量逼近甚至超过这些上限时,系统就会进入不稳定状态,表现为超时、报错、阻塞,甚至整体不可用。
所以高并发处理的第一性原理,不是无限制地压缩单次请求的处理速度,而是通过架构设计和技术手段,让系统在极端流量下仍然保持“可控”的状态。这个“可控”,就是业务抗峰能力的基础。
1.2 搞懂几个核心指标:QPS、RT、并发数
聊高并发之前,先把几个高频指标说清楚,因为后面所有方案选型和容量评估,都建立在量化这些指标的基础上。
| 指标 | 全称 | 含义 | 类比 |
|---|---|---|---|
| QPS | Queries Per Second | 每秒查询/请求次数 | 每秒有多少顾客进店 |
| RT | Response Time | 平均响应时间 | 每笔交易耗时 |
| 并发数 | Concurrent Users | 系统同时处理的请求数 | 店里同时有多少顾客在结账 |
| TPS | Transactions Per Second | 每秒事务处理数 | 每秒完成多少笔完整交易 |
| TP99 | 99th Percentile | 99%的请求耗时在此值以内 | 绝大多数顾客的等待上限 |
这四个指标之间有一个重要的关系公式,也是业内做容量评估时最常用的:并发数 = QPS × RT。
举个例子,如果一个接口的QPS是2000,平均RT是200毫秒(0.2秒),那么系统同时处理的请求数就是 2000 × 0.2 = 400。如果这个接口的服务是单实例部署,每台机器的线程池设置是200,那么至少需要2台机器的线程池才能扛住,这还没算流量毛刺和机器损耗。
做容量规划的时候,不能只看平均RT,还要看TP99,因为平均RT会被大量快速请求拉低,掩盖长尾耗时的问题。我见过太多系统,平均RT看起来只有150毫秒,但TP99已经到2秒了,这种系统的稳定性其实很差,一旦流量上来,长尾请求会拖垮整体。
1.3 业务抗峰的典型场景
搞懂了指标之后,再来看业务抗峰具体面对什么场景。最典型的有这么几类:
- 电商大促:比如618、双11,流量曲线在零点附近呈脉冲状拉升,涨幅可能是日常的50倍以上,持续几十分钟到几小时。
- 秒杀/抢购:小米新品首发、门票抢购,瞬间涌入的流量可达平时的数百倍,持续几秒到几分钟,但破坏力极大。
- 热点事件:突发新闻、热搜话题带来的瞬时访问洪峰,没有预兆,没有规律,持续时间不确定。
- 集中放号/名单公布:某些政务或行业性质的集中放号、成绩查询、摇号结果公布,流量在公告时间点瞬间到达。
- 大规模营销活动:优惠券发放、直播间的团购秒杀,流量集中在活动开始的瞬间。
这些场景都有一个共同特征:在极短时间内,流量远超出系统日常承载能力。如果按照峰值流量来长期准备资源,成本会高得离谱;如果按日常流量准备资源,峰值一来系统就崩。高并发处理和业务抗峰的实质,就是在这两者之间寻找一个平衡——用有限资源,扛住极端流量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 业务抗峰的架构设计核心
2.1 无状态化:弹性伸缩的前提
业务抗峰的第一步,不是加机器,而是确保系统可以加机器。很多系统加不了机器,是因为“有状态”——用户的登录态、业务数据、临时数据都绑在单台机器上,一旦扩容,新机器上没有这些状态,请求处理不了。
状态分为两类:会话状态和业务状态。会话状态就是用户的登录信息、临时写入的购物车数据等,这类数据应该从应用内存中搬出来,放到Redis这类集中式缓存中。业务状态则是订单数据、库存数据这类需要强一致性的数据,这些必须放在数据库等持久化存储中,应用层不保存。
应用层保持无状态之后,整个系统就像一排长得一模一样的“服务节点”,任何一个节点都可以处理任何请求。这时候水平扩容才有意义:流量高了,在负载均衡后面加节点就行;流量降了,缩掉节点节约成本。
这里有一个实操建议:写业务代码的时候,不要在应用内存中缓存任何“跟用户相关”的数据。一些团队图方便,把用户维度的小数据放在本地内存里,美其名曰“优化性能”,结果一扩容数据就串,排查半天才发现问题。业务抗峰的前提是弹性伸缩,弹性伸缩的前提是无状态。
2.2 分层防护:流量从入口就被拦截
抗峰能力强系统的第二个特征是“分层防护”。意思是,流量不要一进来就直接打到数据库,而是层层削减,让最核心的业务逻辑处理那些真正需要处理的请求。
典型的链路是:
- 客户端层:页面静态化、本地缓存,大部分读请求可以不经过网络,直接在客户端就被“消化”掉。
- CDN层:静态资源(图片、JS、CSS)全部走CDN,本质是把流量分散到离用户最近的边缘节点。
- 接入层:Nginx、LVS这一层做负载均衡,同时可以做简单的限流、黑白名单过滤。
- 应用层:做业务处理、本地缓存、分布式缓存读取,必要时承接写请求。
- 数据层:数据库、缓存、消息队列,这一层是整个系统抗峰能力的天花板。
这条链路上每一层都承担不同的职责,而且每一层都应该有“吞吐上限”。如果所有流量都穿透到数据库,数据库连接一打满,整个系统就会因为数据库拖累而连环崩溃。分层防护的本质,是让每一层都做“减法”:
- 客户端层挡住重复请求
- CDN层挡住静态资源流量
- 接入层限流拒绝超额流量
- 应用层通过缓存减少对数据库的查询
- 数据层通过读写分离、分库分表扩展容量
2.3 隔离设计:避免“单点故障传染”
高并发场景下,最容易出现的问题不是所有服务一起挂,而是某一个薄弱环节先挂,然后把整个链路拖垮。例如商品服务挂了,商品详情接口超时,调用它的页面服务线程被占用;页面服务线程池被打满,其他依赖它的接口也全部超时。
业务抗峰的架构必须在设计阶段就做好隔离。隔离手段有几层:
- 进程隔离:不同业务用不同的应用服务,比如订单服务和商品服务不要部署在同一个进程里。
- 线程池隔离:即使同一个系统中,不同依赖也应该用独立的线程池。比如商品查询和库存扣减的远程调用线程池分开,商品查询的老接口出问题,不能影响库存扣减。
- 集群隔离:核心业务和边缘业务分集群部署,大促时优先保障核心链路资源。
- 区域隔离:按渠道、按用户群体分开部署,某一部分出问题可以“甩掉”一部分流量,保住整体可用性。
隔离的本质,是控制爆炸半径。一个节点出问题,影响范围可以在设计上限内,不会蔓延到整个系统。这个意识需要渗透到架构评审、代码落地和容量规划的每一个环节。
3. 业务抗峰的核心技术组件与落地
前面讲的是架构层面的思路,这一部分讲具体的技术组件和落地方法。业务抗峰有几个绕不开的通用组件:缓存、消息队列、限流、熔断降级。这四个组件几乎出现在所有高并发系统的标配里。
3.1 缓存:抗住读峰值的第一道防线
3.1.1 缓存分几层、各自放什么
缓存是应对读多写少场景最有效的武器。我在实际项目里的分法是三层缓存:
- 本地缓存(L1):放在应用进程内,例如 Caffeine、Guava Cache,毫秒级响应,适合放一些粒度细、实时性要求不高的数据,例如商品的基础属性、配置信息。
- 分布式缓存(L2):主流是 Redis Cluster,适合放共享的高频访问数据,例如商品的库存状态、用户的购物车信息、活动的参与标记。
- 数据库(L3):真正的持久化存储,缓存未命中才走这一层。
本地缓存的优点是快,很关键的是省去了网络开销,单机每秒可以扛几十万次读。缺点是数据一致性难保证,因为每台机器的缓存是独立的。所以本地缓存适合放那些“变了也无伤大雅”的数据,比如商品描述、分类列表;如果数据错了会造成资损,比如库存数量,那就必须走Redis或数据库。
实践中,我在大促期间会采用“本地缓存 + Redis”的两级方案,本地缓存过期时间控制在30秒以内,Redis过期时间控制在3到5分钟,落后这个时间窗口是可以接受的,但系统能抗住的流量会提升一个数量级。
3.1.2 缓存穿透、击穿、雪崩的应对
这三个问题是缓存使用中最常见也最容易踩的坑,几乎每次高并发故障复盘都能看到它们的身影。
缓存穿透是指查询一个不存在的数据,缓存里没有,数据库里也没有,导致请求每次都穿透到数据库。如果攻击者恶意构造一批不存在的ID,数据库会被打到挂掉。解决方案:一是布隆过滤器,在缓存之前加一层判断,把不存在的ID直接拦截掉;二是缓存空值,即使查询结果为空,也在缓存里存一个null标记,过期时间设置短一点,比如60秒,防止缓存空间被占满。
缓存击穿是指某个热点key在过期的一瞬间,大量请求同时打到数据库。比如某个爆款商品的详情页,Redis里存的key正好过期了,同时涌进来几万个请求,全部穿透到数据库。解决方案:一是热点key不设置过期时间,改成由后台任务主动更新;二是使用互斥锁,只让一个请求去数据库查,其他请求等待结果;三是热点key的过期时间加随机值,避免同时过期。
缓存雪崩是指大量key在同一时间到期,导致大量请求同时打到数据库。很多团队在设置过期时间时习惯统一用某个固定值,比如“都设成10分钟”,这种做法很危险。解决方案很简单:过期时间设置一个固定基础值再加上一个随机偏移量,比如 600 + random(0, 300) 秒,错开过期时间点。
3.2 消息队列:削峰填谷的关键角色
3.2.1 为什么说消息队列能“削峰”
消息队列在业务抗峰中承担的角色是“蓄水池”。当流量瞬间飙到每秒几万时,后端核心系统的处理能力可能只有每秒两三千,硬扛必定崩溃。引入消息队列之后,请求先写入MQ,由MQ承接海量瞬时流量,后端消费者按照自己的节奏去消费,在流量低谷期继续消费积压的数据。
这就是“削峰填谷”:峰值流量被削掉了,谷底期的资源被填上了。典型的场景是秒杀流程:用户的秒杀请求先写入MQ,而不是直接操作数据库扣减库存;消费者服务从MQ拉取消息,再去做库存扣减、生成订单。用户看到的反馈是“请求已接收”,后面系统异步完成真正的业务操作。
这个方案有一个核心原则:用户能接受的异步操作才走MQ。像秒杀、优惠券发放这类业务,用户只需要知道自己“抢到了”,稍后系统再创建订单也是可接受的。但像支付、转账这类强一致性操作,就必须实时同步完成,不能随便异步化。
3.2.2 消息积压怎么办
削峰填谷有一个副作用——消息积压。大促期间,如果生产速率远大于消费速率,消息积压量会快速上升,如果不加控制,积压到一定量级可能导致消息延迟过高,业务受损。
应对积压的思路有三个方向:
- 提升消费速率:增加消费者实例数量、优化消费逻辑的耗时。比如消费消息时把单条消息的单次处理改为批量处理,大幅度减少网络IO次数。
- 临时降级:非核心逻辑在峰值期间先不处理,比如订单确认后的短信通知、积分赠送这些操作,可以延迟到流量低谷再处理。
- 监控告警:一定盯好消费堆积量,我一般会设置两个阈值:一个警告阈值,堆积超过1万条就告警;一个严重阈值,堆积超过10万条就需要运维介入,考虑扩容消费者。
这里有一个实操经验:配置消费者的处理速率,尽量不要等于生产速率,给消费端留出至少20%的余量。否则一旦出现轻微波动,积压就会像滚雪球一样越来越大。
3.3 限流:把流量控制在系统能承受的范围内
3.3.1 限流算法怎么选
限流是业务抗峰的最后一个兜底手段。它的作用是在流量已经超出系统承载能力时,主动拒绝一部分请求,保障系统整体的可用性。限流算法有四种常见方案,我整理成表格方便对比:
| 算法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定窗口计数 | 固定时间窗口内计数,超过阈值拒绝 | 实现简单 | 窗口边界流量可能翻倍 | 简单接口限流 |
| 滑动窗口计数 | 把时间窗口细分成多个小格,滑动判断 | 比固定窗口平滑 | 精度取决于格子数 | 对准确性有要求的场景 |
| 漏桶算法 | 请求先进入桶内,底部以固定速率流出 | 流量绝对均匀 | 无法应对突发流量 | 需要平滑出流的场景 |
| 令牌桶算法 | 桶内存令牌,请求需要拿令牌才能通过 | 允许一定突发流量 | 实现稍复杂 | 大多数互联网场景首选 |
实际业务中,我用的最多的是令牌桶算法。这个算法允许一定程度的突发流量,同时平滑整体速率,比较符合互联网流量的真实特征。比如系统能承受的峰值是5000 QPS,那设置令牌桶每秒生产5000个令牌,桶容量设置成10000,短时间内可以允许最多10000个请求通过缓冲,但整体速率还是会被控制在5000 QPS左右。
3.3.2 限流阈值怎么定
限流阈值不是拍脑袋定的,而是通过压测得出来的。常规流程是先对核心接口做全链路压测,把QPS逐步推上去,观察系统的RT、错误率、CPU、内存变化,找到那个“再往上加流量,系统指标就会恶化”的临界值。比如压测发现某接口在6000 QPS时,RT从200毫秒涨到800毫秒,错误率开始上升,那么这个接口的限流阈值就设置为5000 QPS左右,留出20%的余量。
限流粒度要区分场景。粗粒度的限流(比如整个应用统一限流)实现简单,但会导致某个耗资源接口拖累其他接口;细粒度的限流(按接口、按用户、按来源)更精准,但配置和维护成本高。我的建议是:核心接口单独配置阈值,非核心接口可以走统一阈值。
3.4 熔断与降级:系统自我保护机制
限流是在入口处“拦住超出的流量”,熔断是在下游出现故障时“主动断开依赖”,防止故障扩散。
熔断机制有三个状态:
- 关闭:正常运行,请求正常通过。
- 打开:请求直接返回预设的fallback(兜底数据),不再调用下游。
- 半开:过一段时间放少量试探请求,如果成功,熔断器关闭;如果失败,继续保持打开状态。
触发的条件一般有两个:错误率达到一定比例(比如50%),或者请求超时率超过阈值。当依赖的下游服务出现问题时,熔断器打开后,上游的请求就不会再打到已经故障的下游,给下游留出恢复的时间。
降级和熔断常常一起出现。降级是一种主动策略,当系统资源紧张时,主动牺牲一些非核心功能,保住核心功能。典型的做法有:
- 商品详情页降级为返回缓存数据,即使数据不是最新的
- 推荐服务挂了,降级为返回默认推荐列表
- 评价、收藏这类非核心功能接口,在大促期间关闭或者延迟
降级的本质是“丢车保帅”,在资源有限的情况下,保证用户的核心流程能够走通。这个策略要在系统设计时就预留好,而不是等故障发生后再临时改代码。
4. 业务抗峰的实战落地套路
4.1 量化评估:先知道峰值有多高
做业务抗峰,第一步不是写代码,而是评估流量。没有量化数据,所有方案都是空谈。评估流量有几个关键问题要回答:
- 峰值QPS是多少?翻遍历史监控数据,找到峰值那条曲线,评估比历史峰值还要高的场景。
- 持续多长时间?峰值持续30分钟和持续3小时,方案完全不一样。短时峰值可以靠限流和消息队列“硬扛”,长时高水位必须扩容。
- 核心链路的单请求RT是多少?RT决定并发数,并发数决定机器数量。
- 数据库的容量上限是多少?数据库连接池、慢SQL、锁竞争,都可能成为瓶颈。
有一个简单实用的计算公式:集群总并发数 = QPS × RT,然后根据单机的线程池容量,推算出需要多少台机器。比如核心接口QPS预计3万,RT压测平均200毫秒,那总并发 = 30000 × 0.2 = 6000。如果单机线程池配置500并发,最小需要 6000 / 500 = 12台,加上20%到30%的缓冲,要准备16台左右。
4.2 全链路压测:抗峰能力的验收标准
很多团队做高并发保障,最怕的不是没做方案,而是方案做完了不知道到底能不能扛住。这时候全链路压测就是唯一的验收手段。
全链路压测和普通接口压测的区别在于,它要覆盖从网关到应用再到数据库的完整链路,模拟真实流量模型,而不是单独压某个接口。实际操作中,有几个关键点需要注意:
- 压测数据要脱敏:不能拿生产环境的真实数据直接压,需要生成一套脱敏后的模拟数据。
- 流量模型要贴近真实:真实流量不是均匀分布的,有毛刺、有波峰,压测时要模拟出这种不规则性。
- 压测隔离:压测流量要打独立标记,压测产生的数据不能污染线上真实数据,常见做法是压测请求的header里带特殊标识,在业务代码中路由到影子库。
- 监控要对齐:压测过程中各个节点的监控指标和告警要实时查看,压测结束后复盘所有瓶颈点,逐个优化。
做全链路压测最痛苦的地方,是它会暴露出平时根本看不出来的问题。比如某个中间件的版本在高并发下会报错、某条SQL在数据量大的时候索引失效、某个第三方依赖的SDK在连接数打满时出现死锁。这些问题在压测中暴露出来,其实是好事——总比大促真正来临的时候爆发要好。
4.3 大促保障的经验清单
经过几次大促保障实战之后,我总结出了一份通用清单,每次做活动前照着打个勾,可以避免大部分低级问题:
- 核心接口的限流阈值是否配置并验证过?
- Redis是否做了水平扩容,热点key是否预埋?
- 消息队列的消费积压是否有人实时盯盘?
- 数据库连接池大小是否根据峰值调整过?
- 下游依赖的熔断阈值是否配置好,fallback逻辑是否就绪?
- 监控大屏是否覆盖核心链路的所有节点?
- 应急预案是否演练过?值班人是否能看懂监控数据?
- 全链路压测是否通过?压测发现的问题是否闭环修复?
有一个经验我每次都会强调:预案不能只写在文档里,要真的演练。我见过有的团队写的应急预案非常详细,又是回滚又是降级,但真到出了事故的时候,操作手册里的命令都跑不通,因为权限没开、账号没配、工具没装。所以每次大促之前,把预案里的关键流程完整走一遍,这个环节不能省。
5. 高并发场景的常见问题与排查
5.1 高频故障与排查思路
高并发场景下的故障,多少会有一些共性特征。我整理了这几年遇到频率最高的问题以及排查时的思路,做成速查表:
| 故障现象 | 可能原因 | 排查手段 | 临时处置 |
|---|---|---|---|
| CPU飙高但流量没怎么涨 | 慢SQL、死循环、GC频繁 | 看线程栈、慢SQL日志、GC日志 | 先降级非核心功能,再定位代码 |
| 接口RT突然变长 | 数据库连接池打满、Redis超时 | 看连接池监控、Redis慢日志 | 扩容连接池,或减少穿透查询 |
| 缓存穿透导致数据库压力大 | 恶意请求或缓存空值未设置 | 查看缓存命中率、监控DB慢查询 | 临时开启空值缓存,或加布隆过滤器 |
| 消息队列积压骤增 | 消费速率下降或生产流量激增 | 看消费者日志、消费线程状态 | 扩容消费者,或临时降级非核心消费逻辑 |
| 内存使用率持续上涨 | 缓存数据过多或内存泄漏 | 堆dump分析、缓存key数量监控 | 临时清理缓存,停掉可疑功能 |
| 网关大量503 | 后端服务过载或熔断打开 | 查看熔断监控、服务调用量 | 扩容或降级,等待服务恢复 |
5.2 几个容易踩的坑
踩过不少坑之后,有几个问题想特别指出来,希望能帮助大家少走弯路。
第一个坑:限流阈值设置过了头。 限流是保护系统的手段,不是业务功能的开关。有些团队为了追求稳定性,把限流阈值设得过于保守,结果正常业务流量就被误伤了。所以限流阈值定完之后,一定要做一次“会不会误伤正常用户”的验证,不能只盯系统稳定性,忽略了业务可用性。
第二个坑:缓存和数据库的一致性处理不当。 高并发下用缓存,最怕的就是缓存里的数据和数据库不一致。我见过不少团队采用“先更新数据库,再删除缓存”的策略,这个策略是对的。但有个细节容易被忽略:删除缓存失败怎么办?如果删除失败,缓存里还是旧数据,后续读请求会一直拿到旧值。解决办法是引入重试机制,或者订阅数据库的binlog变更,异步刷新缓存。
第三个坑:只压测接口,不压测链路。 单独压每个接口,每个接口的表现都很好,但全链路压测一跑就崩。原因往往是链路中间某个环节的容量没跟上,比如Nginx的keepalive连接数、Redis的带宽、消息队列的partition数。高并发处理的瓶颈往往不在业务代码,而在中间件和数据层,全链路压测才是发现真正瓶颈的可靠办法。
第四个坑:预案设计时忽略了“限流拒绝之后的体验”。 当系统确实撑不住、必须限流的时候,被拒绝的用户体验如何?有的系统直接返回一个“系统繁忙”的空白页,用户的第一反应就是刷新,刷新又带来更多流量,恶性循环。比较好的做法是:限流拒绝时,返回一个带有“稍后重试”引导的页面,或者把用户引导到排队等待页面,降低用户的重复请求概率。
6. 写在最后:抗峰能力是设计出来的,不是临时调出来的
说了这么多,回到开头那句话:高并发处理和业务抗峰,本质上是一个设计问题,不是一个调优问题。如果等到大促前一星期才想起来“要准备抗峰了”,那基本上只能靠堆机器硬扛;如果一开始就在架构层面做了无状态化、分层防护、隔离、缓存、异步、限流、熔断降级的设计,那么到了流量高峰,系统的表现其实是“预期之内”的。
我个人在实际操盘过几次大促保障之后,最大的体会是:高并发处理没有银弹,也不是某一两个组件的功劳,它是一个系统性工程。每个组件都在自己的位置上做减法,把流量控制在下一层能承受的范围内,最后的整体系统才能稳定。
最后再分享一个小技巧:每次大促或重大活动结束之后,花半天时间做一次复盘,把监控数据、故障记录、限流日志、扩容记录全部整理出来,这些数据是下一轮容量规划和系统优化的起点。高并发能力的提升不是一次性的项目,而是这个“评估—设计—压测—保障—复盘”循环反复运行的结果。
