
做高并发系统优化这些年我见过太多团队一遇到性能问题就条件反射加机器、上缓存、上消息队列、拆微服务。一顿操作猛如虎回头一看监控QPS 是上去了成本翻了三倍该超时的还是超时该抖动的还是抖动。说白了高并发优化的第一性原则不是“加”而是“减”——减什么就是标题里那两个字降频和降维。降频把频率降下来让系统少干活降维把数据的维度和逻辑的复杂度降下来让系统干简单的活。这两个词我在内部复盘和团队分享里讲了无数次落地效果也远超堆机器。这篇文章不绕弯子直接讲清楚高并发系统的瓶颈到底卡在哪降频和降维分别怎么做再给一个完整的订单系统优化案例拆解以及我这些年踩过的坑。正在做性能调优、MySQL 慢查询优化、接口响应治理的同学这篇能帮你少走不少弯路。1. 高并发性能问题的本质瓶颈永远不在单点1.1 资源消耗的“三座大山”CPU、IO 与锁竞争高并发场景下系统的瓶颈基本逃不出三类资源的争夺CPU、IO 和锁竞争。CPU 好理解序列化、加密解密、正则匹配、复杂的数值计算都是吃 CPU 的大户IO 则是数据库查询、缓存读写、磁盘日志和网络传输这些地方锁竞争更隐蔽Java 里 synchronized、数据库行锁、分布式锁本质都是串行化资源并发一高线程全卡在等待上。真正让人头疼的不是某一个资源满了而是“放大效应”。举个常见例子一个接口本身只要查询 50 毫秒看起来人畜无害但它在高峰期被调用 1 万次那就是累计 500 秒的占用。如果你的接口里还有一个 30 毫秒的外部 HTTP 调用放大效应会更恐怖——线程池被打满、Tomcat 线程耗尽、下游服务被拖垮最后雪崩。所以我说瓶颈永远不在单点而在“单点被高频放大之后”。很多团队在做性能优化的时候习惯盯着单条 SQL 的耗时做文章把一条 200 毫秒的 SQL 优化到 20 毫秒就欢呼雀跃。但如果你换个角度看这条 SQL 在高峰期被调用了多少次可能收获更大把调用频率从每秒 1000 次降到每秒 10 次效果比单次耗时优化两个数量级还要明显。这就是降频和降维的出发点——你不一定非要让单次任务变得更快你可以让它干脆少跑几次或者让它跑的时候别背那么多东西。1.2 频率与维度两个被大多数人忽略的性能变量我在给团队做培训的时候习惯把系统负载抽象成两个变量频率 F 和维度 D。频率 F是单位时间内系统需要处理的请求数、任务数、循环次数维度 D是单次请求需要处理的数据规模、涉及的逻辑复杂度、依赖的服务数量。系统整体的压力大致可以看成 F 和 D 的乘积关系。这个模型的价值在于当系统扛不住的时候其实只有两条路——要么降低 F也就是降频要么降低 D也就是降维。加机器没有改变 F 和 D只是增加了分母加缓存本质上是降频因为重复的查询被本地响应替代了查询频率断崖式下跌加索引本质上是降维因为数据库扫描的数据维度从百万行降到了几十行。很多人没意识到缓存、索引、批量、异步这些看似不相关的优化手段底层逻辑其实都跑不出降频和降维这两个关键词。这里还要解释一个容易混淆的点降频和降维并不是孤立的它们经常协同出现。比如一个定时任务每 5 分钟把全量用户数据拉出来做一次统计这里面既有频率问题5 分钟一次 10 万行全表扫描也有维度问题统计其实只需要 3 个字段。优化的时候先通过字段裁剪降低单次扫描的成本再通过增量同步拉长统计周期两边一起使劲效果才会最大化。我见过太多人只盯着一边结果另半边成了新的瓶颈。2. 降频让系统在时间维度上“慢下来”2.1 降频的本质不是砍功能而是砍重复降频很容易被误解成“降低系统能力”其实完全不是。降频要解决的是“重复劳动”。同一个数据1000 个人查同一个热点 key这 999 次查询本质上都是重复的同一个操作循环里执行一万次 SQL 插入其中有 9999 次是可以合并成一笔批量写的。降频就是把这些重复、高频、但结果没差别的劳动想办法变成低频、合并、延迟处理。我经常用一个生活化类比一个餐厅后厨高峰期如果每个客人的每道菜都单独开火灶台再多也不够用。聪明的厨师会把相近的菜品合并成一个大锅炒把不需要堂食的订单集中到后台统一处理把高峰期暂时不紧急的备菜提前做好。这就是降频——减少开火的次数而不是减少菜品。落到工程上降频的信号通常有三类一是同一数据被反复查询但数据本身变化不频繁比如商品详情、用户昵称这种直接上缓存二是同一类操作被高频触发但结果可合并比如日志上报、埋点数据、批量状态更新这种适合攒批处理三是瞬时流量尖峰明显比如秒杀、抢购、活动开场这种需要用队列削峰填谷。识别出这三类信号降频才有的放矢。2.2 五个真正有效的降频手法附落地思路我在实际项目里把降频落地点归纳成五个请求合并、异步削峰、缓存化、事件驱动化、幂等去重。下面一个一个说清楚。第一个请求合并。典型的场景是前端列表页需要批量拿数据比如一次查 100 个商品的库存传统做法是 for 循环里调 100 次查询接口每一次都走一遍网络和数据库。优化手法是提供一个批量查询接口前端攒够一批 ID 再一次性请求后端用WHERE id IN (...)或者批量缓存 get 一次搞定。接口调用频率从 100 次降成 1 次这就是纯粹的降频。第二个异步削峰。最经典的例子是订单创建后的一堆后续动作发短信、发微信通知、更新统计、清购物车。如果你同步全做用户等得花儿都谢了高峰期更是要命。用消息队列把非核心链路异步化之后订单接口只需要写库和发消息响应时间直接下降一个数量级下游消费端根据自己的处理能力慢慢消化。注意这个手法的关键在于“非核心”三个字别把必须同步返回结果的核心逻辑也甩进队列里。第三个缓存化。这是最普遍的降频手段。本地缓存用 Caffeine分布式缓存用 Redis热点数据缓存命中率做到 90% 以上数据库的查询压力直接降到原来的十分之一。我常说的一个经验是缓存的 key 设计决定了降频的天花板。如果一个缓存 key 只被同一个用户访问那它的复用率就很低如果能设计成被一类用户共享比如“城市维度的商品推荐列表”命中率和降频效果会好很多。第四个事件驱动化。很多系统的性能问题是被轮询拖垮的。前端每隔 3 秒轮询一次订单状态高峰期 1 万个用户就是每秒 3000 多次请求即使每次查询很轻量级上来了照样把服务和数据库压得喘不过气。改成 WebSocket 或 SSE 推送状态变了才主动推给前端轮询频率直接变成 0。后端也一样定时任务频繁扫表不如用事件表加延迟队列有变化才处理。第五个幂等去重。高频场景下大量请求其实是重复的比如前端按钮被用户连点两次、消息队列消费超时重投、网关层重试机制。如果不做幂等同一个订单可能被创建两遍同一个消息被消费两次。加一个基于唯一键的幂等表或者用 Redis setnx 做短时间窗口的去重能把大量无效重复请求挡在业务逻辑之外。这五招组合起来用系统的有效请求量通常会下降一大半。2.3 降频带来的副作用缓存一致性、延迟与积压降频不是没有代价的。第一个代价是缓存一致性。缓存了数据更新数据库之后缓存可能短暂是旧的极端情况下还会出现脏数据。我的建议是能接受最终一致性的场景才用缓存严格要求强一致的场景不要为了性能牺牲正确性。更新策略上优先用延迟双删或者 Canal 监听 binlog 异步刷新不要每次都手动更新缓存容易漏。第二个代价是延迟。批量处理和异步削峰本质上都是把即时响应换成了延迟响应。用户提交一个操作可能需要等几秒甚至几分钟才能看到结果。这个在业务上要能接受比如“提交成功正在处理中”这种状态产品设计上就要做好准备而不是让用户干等着不知道发生了什么。第三个代价是积压风险。削峰填谷最怕的是削峰之后谷没填上消息越积越多。一旦下游消费能力跟不上消息队列里的积压会像滚雪球一样增长等到业务反应过来已经延迟了半小时。所以做任何异步化改造必须配套监控队列积压数、消费延迟时间、失败重试次数这三个指标一个都不能少。我自己就吃过亏秒杀活动当天队列积压了 20 万条消息所有订单状态更新延迟了 40 分钟客服电话被打爆那次之后我把所有队列都加了积压告警。3. 降维让系统在空间维度上“变简单”3.1 降维的本质让每一步操作都更轻降维降的是数据规模和逻辑复杂度。很多时候系统慢不是频率高而是每一次请求都要处理太多东西。举例一个订单详情程序里先查订单表再查用户表再查商品表再查物流表四个 join 下来数据量指数量级放大。再比如一个列表查询代码里select *一张表 50 个字段全部捞出来序列化、网络传输、前端解析用户其实只需要其中 3 个字段。这些都是典型的“高维度”问题。降维通俗讲就是把“大而全”变成“小而精”。同样是查数据从全表扫描变成索引扫描是数据维度的下降从查 50 个字段变成查 5 个字段是字段维度的下降从 join 4 张表变成查一张冗余宽表是关系维度的下降从一个大事务处理 1 万行数据变成 100 个事务各处理 100 行是事务粒度的下降。每一样都能让系统的单次开销变小进而提升整体吞吐。我说过一句很接地气的话高可用、高并发的秘诀往往不是把事情做得更复杂而是把每一步都做得更轻。降维就是干这个事的。很多系统在初期快速迭代时为了省事会把一堆逻辑塞在同一个接口里数据模型也尽可能复用同一张表。等并发上来之后这些“省事”成了最大的麻烦——接口重、数据杂、依赖多。降维就是给这些历史包袱做减法的手术刀。3.2 四个核心落地招式查询瘦身、关系降维、数据分治、事务拆分降维落到操作层面我总结了四招查询瘦身、关系降维、数据分治、事务拆分。查询瘦身是见效最快的一招。两个动作一是禁止select *明确列出需要的字段二是设计合理的联合索引让查询走覆盖索引。举个 MySQL 的例子一个订单列表查询条件WHERE user_id 123 AND status 1不加索引的时候全表扫描可能扫 500 万行耗时 1.2 秒。加了联合索引(user_id, status)再配合只查id, order_no, amount三个字段用上覆盖索引扫描行数降到几十行耗时降到 0.03 秒。性能提升 40 倍代码一行没改就是字段和索引的维度降下来了。关系降维核心是用冗余字段换 join。在数据库查询里每次 join 都意味着额外的 IO 和内存消耗join 的表越多查询计划越复杂性能越不可控。常见手法是在订单表里冗余一个商品快照字段把商品名称、图片、单价在下单时复制过来查询订单详情就不用再去 join 商品表了。商品信息后续改了也不影响历史订单的展示因为用户看到的就是下单当时的快照。这也是很多电商系统的标准做法。数据分治就是分库分表分区。当一张表的数据量超过千万级甚至亿级即使有索引查询性能也会明显下滑。这时候按时间、按用户 ID、按业务维度做分区或分表让每次查询只在附近的数据块里找数据维度的量级直接缩小几个数量级。注意分治不是越细越好分太细之后跨表查询、聚合统计、路由管理都会变复杂一般单表数据量控制在 500 万行以内或者按时间区间保证单分区数据量可控是比较稳妥的点。事务拆分就是不要让一个大事务“包揽一切”。事务越大持有锁的时间越长冲突的概率越高。一个更新 1 万行数据的单事务在高并发下几乎必然造成大量的锁等待超时。把它拆成 100 个事务、每个事务处理 100 行虽然总耗时可能差不多但对并发的友好度完全不同。我见过一个库存扣减的接口高峰期大量报错排查发现就是事务里除了扣库存还带着更新订单状态、写操作日志、刷新缓存等一串操作锁的范围被无限放大。拆出来之后核心的扣库存操作只保留必要动作问题立刻缓解。3.3 降维的边界别把系统搞成垃圾场降维同样是双刃剑过度降维会让系统变成一团浆糊。最典型的就是为了减少 join 而疯狂冗余字段结果同一个数据在五六张表里重复存储每次更新都要同步改好几张表稍不注意就数据不一致。我的经验是冗余字段只适合“写少读多”且对一致性要求不高的场景比如商品快照这种历史数据而不是核心状态字段这种需要强一致的。另一个暴雷点是索引过多。很多人一听到降维度就疯狂加索引一张表加了个七八个索引写操作每次都要更新所有索引写放大效应非常明显。而且优化器面对太多索引可能选错执行计划适得其反。我一般建议单表索引控制在 5 个以内并且每个索引都要有明确的查询场景支撑没有场景的索引就是负债。数据分治也一样分库分表之后跨库查询、分布式事务、全局 ID 生成、聚合统计全都变麻烦。如果刚开始数据量只有几百万行非要去搞分库分表那是给自己挖坑。降维的边界在于在满足业务需求的前提下用最小的复杂度换取最大的性能提升。复杂度的增加本身就是一种“升维”所以在做降维的时候要时刻问自己这套方案是不是让系统的整体复杂度变高了4. 实战案例一个订单系统从 850ms 到 45ms 的降频降维实录4.1 项目背景与压测基线去年我接手了一个电商平台的订单中心优化。背景是平台用户量稳步增长高峰期订单接口 QPS 到了 2000系统开始频繁报警。监控数据是这样的下单接口平均响应时间 850msP99 已经飙到 3.2 秒订单列表查询接口数据库 CPU 持续 90% 以上搜索订单时用户经常刷不出数据。而且随着大促活动临近预估峰值 QPS 要翻三倍这个状态明显扛不住。先做基线压测和瓶颈定位。压测环境配了 4 台 8C16G 的应用服务器、2 台 16C64G 的数据库从压测结果看应用服务器 CPU 使用率只有 40%但数据库 CPU 已经打满慢查询日志里一堆耗时超过 1 秒的 SQL。再看代码问题非常典型订单列表查询select *加 join 了 4 张表一张表就有 500 万行数据库存扣减在一个大事务里还带着一堆不相关逻辑订单状态变更疯狂推送 MQ下游消费不过来消息积压严重前端每 3 秒轮询一次订单状态高峰期轮询请求占了应用服务器 30% 的处理量。这就是一个典型的“高频率 高维度”双高问题。优化思路非常清晰能降频的降频能降维的降维。下面看具体实施。4.2 降频改造轮询改推送、消息合并、异步化降频这边做了三件事。第一件前端订单状态查询从 3 秒轮询改成了 WebSocket 推送。订单状态无非是待支付、已支付、已发货、已完成、已取消这几种用户支付之后状态才变化平时根本没必要高频轮询。改造之后前端只有在状态变更时才收到推送状态查询的请求量直接归零应用服务器负载瞬间降下来一大块。第二件订单创建后的非核心链路全部异步化。原下单接口里同步做了扣库存、生成订单、更新用户统计、发送 Webhook 通知、写操作日志、发短信邮件等一长串操作。改造后只保留扣库存和生成订单两个核心动作其余全部投递到 MQ由下游消费者异步处理。下单接口的逻辑瞬间清爽了RT 大幅下降。第三件MQ 推送做了批量合并。原来每个订单状态变更都会单独发一条消息下游收到一条处理一条。改造后把状态变更消息攒批发送下游也改成批量消费同样的业务量消息数量下降了 70% 以上。这里需要注意批量合并会引入一定的处理延迟我们通过控制攒批窗口在 200 毫秒以内既保证了吞吐又不会对用户体验造成明显影响。4.3 降维改造重写查询、冗余快照、重建索引降维这边第一件是重写订单列表查询。原代码里select *加 join 4 张表改成只查订单表必需的字段配合联合索引走覆盖索引。另外把 join 的用户表、商品表、物流表信息在表设计上做了冗余订单表新增了用户昵称、商品名称、商品图片、物流单号这几个高频展示字段查询从 4 表 join 变成单表查询。多花了一点存储换来了数量级的性能提升。第二件是库存扣减事务的拆分。原来一个事务里扣库存、更新订单、写日志、刷缓存全包了锁的范围非常大。改造后核心事务只保留“扣库存”和“生成订单主记录”两个必要动作其他全部移到事务外或者异步执行。这里有个细节事务拆了之后要处理“事务内操作成功但事务外操作失败”的补偿问题我们的方案是引入一个本地事件表事务内写数据的同时插入一条待处理事件事务提交后再去异步投递保证链路可靠。第三件是订单表的索引重建。原来一张表上建了 7 个索引但很多索引根本没有实际查询场景支撑反而拖慢了写入。我用慢日志和 explain 分析后把索引精简为 3 个联合索引(user_id, status)覆盖订单列表查询索引(create_time)覆盖时间范围查询唯一索引(order_no)保证幂等。同时给大字段做了裁剪把不常查询的大文本字段挪到扩展表主表变得更瘦查询性能明显提升。4.4 优化效果同一套机器QPS 翻了四倍优化的效果可以看这张对比表指标优化前优化后变化订单接口 QPS峰值 2000稳定 8000提升 4 倍平均响应时间 RT850ms45ms降低 94.7%P99 延迟3.2s120ms降低 96.2%数据库 CPU90% 以上30% 左右降低六成MQ 消息积压频繁告警基本为 0消除积压单次列表查询 SQL 耗时1.2s0.03s降低 97.5%最让我意外的是数据库 CPU。优化前都快被打满了优化后同样的机器使用率只有 30%看来之前的压力基本都是无效查询和重复查询撑起来的。同一套硬件支撑的业务量翻了几倍这就是降频和降维叠加的效果。这次优化的经验我沉淀成了团队内部的三条准则第一遇到性能问题先看请求量再看单次成本不要一上来就优化单点耗时第二任何优化都要有监控数据支撑压测-定位-改造-验证是一个闭环缺一环都容易拍脑袋第三降频和降维永远一起考虑只做一边另一边很快就会成为新的瓶颈。5. 常见问题排查与避坑指南5.1 快速定位性能瓶颈的工具链组合很多同学问我是怎么在项目里快速定位问题的我把常用的工具链分享出来。系统层面top看 CPU 和负载vmstat 1看上下文切换和 CPU 分布iostat -x看磁盘 IO 队列free看内存和 swap。这是最基础的排障三板斧一般能定位到资源瓶颈是在 CPU、IO 还是内存上。应用层面Java 服务可以用jstack抓线程快照看线程都在等什么jstat看 GC 频率和耗时Arthas 的dashboard可以实时看线程池、内存、类加载情况还能直接反编译线上代码排障利器。数据库层面MySQL 开慢查询日志EXPLAIN分析执行计划SHOW PROCESSLIST看当前线程状态performance_schema看锁等待和 IO 热点。链路追踪用 SkyWalking 或者 Zipkin能直接看到一次请求在各个服务之间的耗时分布快速圈定瓶颈节点。排查的顺序我建议是先看资源CPU/IO/内存再看应用线程然后看数据库慢日志最后用链路追踪把整个调用链拉出来。不要一上来就翻代码找问题没有数据支撑的猜测基本都是浪费时间。我曾经帮一个团队排查问题他们怀疑是缓存框架的 bug调试了两天最后发现是同一张表的一个字段没有索引全表扫描把数据库拖垮了。这就是没有按顺序排查的教训。5.2 高频问题排查速查表我把实际运维中遇到的高频问题整理成了一张速查表方便大家在群里讨论问题时快速对齐症状可能原因优先排查方向数据库 CPU 100%慢 SQL 全表扫描、无索引或索引失效慢查询日志、EXPLAIN 看 type 是否为 ALL接口 RT 飙升但 CPU 不高锁等待、IO 瓶颈、下游接口变慢线程快照、链路追踪、检查锁和依赖并发一高就超时低峰正常线程池配置过小、连接池耗尽jstack 看线程状态、检查池配置消息队列积压暴涨消费能力不足、死信堆积、批量处理不当消费端耗时监控、重试和死信配置缓存命中率低key 粒度太细、过期时间太短、数据访问不均Redis 命中率监控、key 设计复盘大促瞬时流量打垮接口缺削峰手段、系统没有降级兜底MQ 削峰、限流降级、缓存热点分页越深越慢大 offset 扫描数据维度过高游标分页、限制最大页数、覆盖索引这张表的价值在于帮助大家建立“症状到方向”的快速映射。实际工作中很多问题都是多个因素叠加的比如数据库 CPU 高可能同时有慢 SQL 和缓存命中率低两个原因。所以排查的时候不要只找一个答案要把相关的可能性都过一遍然后再确定优先级最高的那个。5.3 我总结的几条优化铁律做了这么多项目我把经验浓缩成几条铁律。第一条先降频再降维。为什么降频往往是“快赢项”改动可能只是加个缓存、加个批量收益立竿见影而且风险相对可控。降维通常涉及表结构变更、代码重构周期长风险大。先把容易的做了系统压力降下来再做深度优化节奏更稳。第二条每一次降频都要有“可观测性”兜底。把请求频率降下来之后你原来靠高频请求“掩盖”掉的问题会浮出水面比如之前靠轮询不断纠错的数据异常现在推送不到了之前幂等靠数据库唯一索引挡住的重复请求现在要额外处理。没有完善的日志和监控降频之后出了问题你都不知道是该回滚还是继续优化。第三条降维不要动核心链路的核心字段。你在订单表里加冗余字段没问题但如果你把订单状态、支付金额这种核心字段也做成冗余存储那就等着出大事故吧。降维的目的是让系统更简单高效不是违背数据设计的根本约束。核心数据永远要保持唯一的权威来源冗余只为了查询展示绝不承担核心逻辑。第四条没有压测不要轻信任何优化。我见过太多“优化完感觉变快了”的错觉。代码改动之后一定要做一轮压测和优化前的基线做对比用数据说话。至少要看三个指标QPS、RT 曲线、错误率。只有这三个指标都在可接受范围内优化才能算数。最后再分享一个小技巧写到这里该说的干货都说了最后再分享一个我一直在用的习惯每次做完性能优化我都会顺手写一份“优化前后的对比笔记”记录当时的监控截图、慢 SQL、压测数据、改动点、踩过的坑。这个东西短期看没什么用但半年之后当同样的性能问题再次出现翻一翻笔记能帮你节省大量排查时间。我自己的笔记里已经积累了上百个类似的案例很多问题的解决方案都是直接复用之前的思路。另外想多说一句降频和降维看着简单真正执行起来非常考验对业务的理解深度。你只有知道哪些请求是重复的、哪些数据是可以裁剪的、哪些环节是不必要的才敢下手去优化。所以我建议做技术的同学不要只盯着技术本身多去了解业务场景多站在用户视角想想“这个数据真的需要吗”“这个步骤真的不能省吗”。很多时候最有效的性能优化不是来自高深的技术而是来自对业务本质的深刻理解。