☰
高并发性能优化实战:从瓶颈定位到水平扩展的五层框架
2026/10/6 16:30:21 网站建设 项目流程

1. 面试官到底想考什么:并发性能问题的思维框架

如果你去面试后端开发岗位,十有八九会碰到这么一句开场白:"聊聊你怎么提升项目并发性能?"

我当过面试官,也在候选人位置上被问过无数次。老实说,这个问题的杀伤力不在问题本身,而在它的开放性。它不像"HashMap和ConcurrentHashMap的区别"那样有标准答案,也不像"如何实现一个分布式锁"那样有明确抓手。它是一个典型的"系统设计题",考察的是你对整个技术栈的理解深度、排查问题的思路,以及踩坑之后的真实沉淀。

大部分候选人是怎么答的?上来就背八股:加缓存、加消息队列、分库分表、读写分离、水平扩容……噼里啪啦说了一堆名词,听起来很全,但面试官只要接着追问一句"你怎么判断当前系统需要加缓存?加了之后数据一致性怎么保证?你的数据库到底是不是瓶颈?"就基本露馅了。

真正有水平的回答,不是名词的堆砌,而是一条有顺序、有依据、有取舍的思路链路。顺序是什么?是先搞清楚现状再谈优化,先定位瓶颈再动手调优;依据是什么?是数据、监控、压测结果,而不是拍脑袋;取舍是什么?是明白每个方案都有代价,知道什么阶段该做什么事。

我自己的回答框架一直是五层递进:

第一层,确认系统现状。没有压测、没有监控数据之前,一切优化都是耍流氓。得先知道并发量到底是多少、瓶颈在哪个环节、QPS和RT的基线值是多少。

第二层,代码与单机层面的优化。这个层面不花一分钱,也是最容易出效果的地方:锁的粒度、线程池参数、批量处理、异步化、连接池配置、GC调优。

第三层,缓存与读写分离。把热点数据和读多写少的流量挡在数据库之前。

第四层,集群与水平扩展。无状态化、负载均衡、分库分表,用机器数量换性能上限。

第五层,流量治理与保护。限流、降级、熔断,保证系统在远超预期的流量下不死掉。

这个递进关系很重要——它体现的是一个工程师从单点走向系统、从局部走向全局的思考过程。面试官问"如何提升并发性能",其实是想看你脑子里有没有这棵完整的技能树。

这篇文章我就把这五层展开讲透,每层都会附上我实际项目里的配置参数、踩坑经历和判断依据,希望能帮你在面试时不仅"有的说",还能"说得有底气"。

2. 第一板斧:别急着优化,先把瓶颈"测"出来

很多刚工作两三年的同学有个通病:系统一慢就想加缓存、加机器,好像并发性能就等于中间件数量。但真实情况是,80%的性能问题不需要架构级改造,只需要找到那个具体的热点。

我常用一套"三步定位法":

第一步,看监控,圈定上下游。公司大概率有APM类的监控系统,先看一个请求从入口到出口的链路耗时分布。如果发现Tomcat线程池里线程大多阻塞在数据库查询上,那你加十台应用服务器也没用;如果发现是某个第三方接口响应慢拖累了整体,那问题根本不在于你自己的并发能力。

第二步,做压测,拿到基线和瓶颈点。用压测工具(我一般用JMeter或者wrk)逐步增加并发数,同时盯着几个核心指标:QPS、RT均值、RT P99、错误率、CPU使用率、内存使用率、GC频率和停顿时间。压测的目的不只是"测出能扛多少量",更重要的是找到首个被打爆的环节——可能是数据库连接池、可能是某个线程池拒绝策略触发、可能是Full GC过于频繁、也可能是带宽被占满。

第三步,排除法,定位到代码级热点。通过Arthas之类的工具,线上热部署查看某个方法的调用耗时、调用次数,甚至可以看它的火焰图。我曾经排查过一个诡异的现象:并发量到200时QPS上不去,单看每个接口RT都很正常,后来发现是日志框架的异步队列满了,同步刷盘把线程全部堵住。这种问题你不去代码级排查,单纯加服务器资源是永远解决不了的。

所以,我的第一个建议是:把压测工具和监控面板熟悉到肌肉记忆的程度。面试时如果被问到"你怎么判断瓶颈在哪",你能说出来"先看APM链路,再用压测逐步加压,盯紧P99和GC,最后用Arthas定位热点方法",这套回答的含金量比直接给出十个优化方案高得多。

压测的时候有几个容易踩的坑,顺手提醒一下:

  • 压测机本身不能成为瓶颈。单机压测时,压测机的CPU、网络连接数、端口数都有可能先打满,导致结果失真。建议从多台压测机一起压,或者至少保证压测机的性能远高于目标服务。
  • 注意RT的P99而不是平均值。平均值会被大量慢请求拉高,但真正影响用户体验的是那些极端慢请求。P99代表最慢的1%,通常P99 > 5倍均值就说明系统出现了明显的长尾延迟。
  • 压测必须分场景。比如"纯读场景""读写混合场景""有热点Key场景"分别压,不同场景下的瓶颈是完全不同的。

这些细节在面试时提出来,面试官一下就知道你是真上过战场的——因为只有真的扛过流量、排过故障的人,才会对这些"测试的测试"如此敏感。

3. 第二板斧:线程模型与锁——单机性能的根基

确认瓶颈之后,大部分情况下你首先会在单机层面做优化。为什么先说单机?因为单机性能是集群性能的地基——你单机能扛1000 QPS,加10台机器才能扛10000;如果单机只能扛100,加100台机器也没用,反而把连接数和资源开销放大到难以承受。

单机层面最重要的两个关键词:线程和锁。

3.1 线程池参数:别再用默认配置了

Java后端最常用的线程池是ThreadPoolExecutor,Spring的@Async、Tomcat的请求线程池、各种中间件的处理线程池,底层基本都是这东西。但很多人对线程池参数的设置还停留在"核心线程数=CPU核数"这种刻板公式上,这其实是最大的误区。

线程池大小的估算,要区分你是CPU密集型还是IO密集型:

  • CPU密集型(比如图像处理、加密计算):线程数≈CPU核数+1,因为线程多了反而会因为上下文切换变慢。
  • IO密集型(比如调用数据库、调用外部API、读写文件):线程数要放宽很多,经验公式是"CPU核数 × (1 + 平均等待时间 / 平均计算时间)"。如果一次请求里计算耗10ms、等数据库响应50ms,那么8核机器可以开到 8×6 = 48 个线程左右。

但请注意,这个公式只能作为起点。我见过一个项目,单接口要调三个外部服务,平均等待时间超过了200ms,把线程池从默认的200调到了400之后,吞吐直接翻倍。线程池参数一定是一个压测调出来的值,不是算出来的值。

还有两个细节必须注意:

  • 队列容量和拒绝策略。我们经常只关注线程数,忽略了队列容量。默认的LinkedBlockingQueue容量是Integer.MAX_VALUE,意味着任务永远排队、线程数永远不会增加到maxPoolSize,这等于你的极端流量来了之后全部堆在队列里,响应时间无限拉长。生产环境要么用有界队列配合合适的拒绝策略,要么用SynchronousQueue这种无缓存队列让线程直接处理。我自己的习惯是:核心业务用有界队列(比如1000),拒绝策略用CallerRunsPolicy——让提交任务的线程自己执行被拒绝的任务,这样既能兜底,又能天然形成反压。
  • 线程池隔离。一定不要把"查订单"和"发短信"这两种完全不同耗时的任务放进同一个线程池。一个慢业务把线程池占满,另一个快业务也跟着被拖死。按业务场景拆成多个独立线程池,互相隔离,是最基础同时也是最容易被忽略的容错设计。

3.2 锁的粒度与无锁化改造

Java提供了Synchronized、ReentrantLock、ReadWriteLock、StampedLock、ConcurrentHashMap、LongAdder、AtomicInteger这一系列从重到轻的同步方案。它们的本质区别,其实就是从"操作系统级别的互斥"到"CPU级别的CAS自旋",再到"线程本地化"的三级跳。

很多人在高并发下性能骤降,就是因为全局用了synchronized锁住了大的代码块。优化方向是清晰的:

  • 缩小锁范围。把整个方法加锁改成只对共享变量加锁。我曾经把一个"库存扣减+订单创建+积分更新"的大事务锁改成只对库存扣减这一个操作加锁,并发量提升了将近30%。
  • 读写分离。用ReentrantReadWriteLock或StampedLock,读操作不阻塞、写操作才加锁。适合"配置读取频繁、更新频率极低"的场景。
  • 用CAS替代互斥锁。对那种"读-改-写"的简单操作(比如计数器递增),用AtomicLong或LongAdder就够了。LongAdder在超高并发下的表现比AtomicLong更好,因为它把单个计数拆成了多个Cell,减少了CAS竞争。
  • 线程本地化。如果每个线程只需要自己的数据副本,用ThreadLocal直接从根上消灭竞争。常见的场景如SimpleDateFormat不是线程安全的,每个线程自己持有一个实例,比全局加锁效率高好几个数量级。

3.3 数据库连接池:容易被忽视的共享资源

还有一个和锁密切相关的隐藏瓶颈——数据库连接池。它本质上是"锁+池化"思想的应用,多线程共享有限的连接,就必须加锁来分配连接,连接数太少就成了并发瓶颈。

Druid和HikariCP我都用过。HikariCP的默认配置是maximumPoolSize=10,这个值在大多数场景太小了。很多人以为加大连接数总是好的,其实不然——数据库本身的并发处理能力有限,连接数超过数据库能同时处理的量之后,一部分连接就会排队等待。maximumPoolSize的估算公式可以粗略参考:((core_count × 2) + effective_spindle_count),其中effective_spindle_count对SSD大概是1、机械硬盘大概是4。4核8线程的机器跑SSD,连接池开10~12是合理起点,再通过压测微调。

判断连接池是否成为瓶颈的方法很简单:压测时看监控面板里activeCount有没有长期接近maximumPoolSize,以及等待获取连接的waitTime有没有持续上涨。有的话,优先加连接池数量,比加应用服务器划算得多——因为连接池溢出往往意味着单个数据库节点已经快到了极限,这时候更应该考虑的是读写分离或缓存,而不是继续堆连接数。

4. 第三板斧:缓存——并发性能的杠杆点

聊完单机,下一个性价比最高的环节就是缓存。缓存的本质是把计算变成存储、把远程调用变成本地调用、把磁盘变内存。它是并发性能优化里投入产出比最高的手段,但同时也是坑最多的手段。

4.1 缓存分层与选型:CPU缓存、本地缓存、分布式缓存

CPU有多级缓存,我们的系统架构也有多级缓存。面试时如果你能说出"缓存要分层设计",这句话本身就加分。

完整的缓存链路从近到远应该是:

  1. 浏览器缓存/CDN:静态资源的请求在还没到你的服务器之前就被消化了。适合图片、CSS、JS等几乎不变的资源。
  2. 本地缓存:Caffeine(Java)或Guava Cache。适合那些几乎不变化且访问极其频繁的数据,比如"地区列表""t字典配置"。本地缓存的访问延迟是纳秒级,因为它根本不需要网络IO。
  3. 分布式缓存:Redis。适合多实例共享的数据,比如用户信息、商品详情。Redis的延迟大概是毫秒级(0.5~1ms),比本地缓存慢两个数量级,但仍然比数据库快一个数量级。
  4. 数据库:最终的数据源。

这里的核心权衡是一致性与性能的取舍。本地缓存速度最快,但多个应用实例之间数据的一致性最难保证;Redis一致性相对容易控制,但有网络开销。

我在实际项目中的做法是"两级缓存":热点数据先在Caffeine里查,没命中再查Redis,还没命中才查数据库,查完之后反向写回两级缓存。Caffeine的过期时间设短一些(比如30秒),Redis的过期时间设长一些(比如10分钟),这样既保证了整体数据的相对新鲜,又用极低成本的本地缓存挡住了大量热点请求。

4.2 缓存穿透、击穿、雪崩:三大经典事故

这三个词是面试必考题,但很多人只记住了定义,实战中的应对方式却很稀薄。这里我把每个问题的触发场景、判断方法和应对措施都列清楚:

问题触发原因判断特征应对措施
穿透缓存和数据库都没有该Key,每次请求都穿透到数据库数据库QPS骤增,但Redis命中率很低1. 布隆过滤器拦截不存在的Key 2. 缓存空值并设置较短过期时间(如60秒)
击穿某个热点Key的缓存刚好过期,大量请求同时打到数据库单个Key的访问QPS极高,缓存过期瞬间数据库压力突增1. 互斥锁重建缓存(只允许一个线程去数据库查询) 2. 逻辑过期(值里存过期时间,线程发现过期后异步刷新缓存)
雪崩大量Key在同一时间过期,或Redis挂掉,请求全部打到数据库数据库CPU/连接数瞬时打满,Redis中有大量批量过期的Key1. Key过期时间加随机值分散 2. Redis高可用(主从+哨兵/集群) 3. 多级缓存兜底 4. 限流降级保护数据库

三个问题里,穿透最容易被忽略但作用最大。现实世界里的恶意访问、爬虫扫全站、或者用户查询不存在的商品ID,都可能造成穿透。布隆过滤器的实现并不复杂——用一个BitMap和几个哈希函数判断Key"一定不在"还是"可能存在",拦截能力极强,而且内存占用非常小,一个亿级的BitMap也就一百多MB。

4.3 缓存一致性:最终一致+延迟双删

缓存最常见的业务问题就是"数据库更新了,缓存还是旧值"。业界没有完美的强一致方案,大家用的都是最终一致性路线。

我实践中用过的方案有三种,各有适用场景:

  • 先更新数据库,再删除缓存(Cache Aside Pattern):这是最常用和最推荐的方案。因为删除缓存这步是幂等的,即使删除失败,等下次缓存过期后也会重建。问题在于更新数据库和删除缓存这两个动作之间有一个并发窗口——可能有请求在更新前读到旧值并写回缓存。应对办法是"延迟双删":先删缓存,更新数据库,休眠几百毫秒再删一次缓存。这个延迟时间要大于"读请求把旧值写回缓存"的耗时。
  • 先更新缓存,再更新数据库:不推荐。缓存更新成功、数据库更新失败,数据就永久不一致了。
  • 消息队列异步同步:更新数据库后发一条消息到MQ,消费者删除对应缓存或更新缓存。好处是解耦、重试方便,坏处是引入额外组件,且有一定延迟。

我的习惯是:核心交易数据用"数据库更新后发MQ清缓存"的方式,保证最终一致且可重试;普通配置类数据直接用"先更新数据库,再删缓存"的简单方案。

这里还要强调一句:任何缓存方案都是有成本方案的,能少用就少用。如果一个接口的QPS本身只有100,数据库完全扛得住,没必要硬上缓存——缓存的引入会带来一致性风险、缓存过期管理成本、前后端联调复杂度。它的使用场景应该是"读多写少且存在明显的热点"。

5. 第四板斧:数据库侧的并发策略

聊完缓存和单机优化,绝大多数项目的并发瓶颈会转移到数据库。数据库是状态的中心,也是并发路径中最难水平扩展的部分。针对数据库,优化手段要分三个层次来说:连接与事务、索引与查询、架构形态。

5.1 事务边界:并发与一致性的最优解

我见过太多人为了"保证数据不丢"把整个业务逻辑包在一个大事务里。比如下单这个操作:要扣库存、生成订单、更新用户积分、发一张优惠券,于是把四个操作全扔进一个事务。这种做法在低并发下没问题,但一旦并发上来,事务持有锁的时间越长,锁竞争就越严重,死锁概率也越高。

优化事务的三条准则:

  • 事务只包含必要操作。一次事务只做"必须原子化"的最小操作。下单场景里,"扣库存+生成订单"是必须原子的,之后的积分、优惠券完全可以拆到另外一个事务里(即使失败了,也可以通过补偿任务补齐)。
  • 把远程调用和外部IO移出事务。在事务里调用Redis、调用第三方HTTP接口是大忌。想象一下:事务开启了,锁拿了,然后你的代码去调用一个超时时间为3秒的第三方接口——这3秒内,所有对这个数据的操作都会被阻塞。
  • 使用更短的查询路径。即使在一个事务内,查询条件也要尽量走索引,否则一条慢查询会把锁持有时间拖长好几个数量级。

这里我不建议教条化地追求"绝不能有大事务"——有些业务确实必须原子,比如转账场景。但如果你每次重构都考虑"最小事务边界",并发能力自然会提升。

5.2 索引与慢查询:数据库并发性能的隐形杀手

并发性能差,有时候不是架构问题,而是一两条慢查询在拖后腿。慢查询对并发的杀伤力远比很多人认为的要大:一条慢SQL把数据库连接占住几百毫秒,那么数据库连接池中所有连接都可能被慢查询占满,其他哪怕是简单的主键查询也被阻塞排队。

排查思路固定且有效:

  1. 开启慢查询日志,定位RT超过阈值(比如200ms)的SQL。
  2. 用EXPLAIN查看执行计划,重点看type是不是ALL(全表扫描)、key有没有真正用上索引、rows扫描了多少行。
  3. 针对高扫描行数的SQL,分析是否可以通过联合索引覆盖、或者把复合查询拆成单索引查询后合并。

我在一线踩过的一个典型案例:某个订单查询列表接口做了大范围时间筛选+状态筛选+模糊搜索的LIKE '%xxx%',必然全表扫描,数据量一上来接口就慢。优化方案是分拆查询条件:精确查询字段走索引,模糊搜索字段限制必须先走索引查出一批ID再关联过滤。数据量级从几万条增长到百万条之后,接口RT从800ms降到了80ms。

5.3 读写分离与分库分表:一定要想清楚再动

当数据库的读流量远大于写流量(常见比例超过10:1)时,读写分离是性价比很高的方案——把多个只读副本挂到主库后面,SELECT请求打向从库,INSERT/UPDATE/DELETE打向主库。常用中间件有ShardingSphere、MyCat,或者直接在业务层做数据源路由。

但有两个隐藏的坑必须注意:

  • 主从延迟。MySQL主从同步默认是异步的,极端情况下从库可能落后主库几百毫秒,某些刚写完就立刻去读的用户会暂时看不到自己的操作结果。解决办法是"写后立即读"的请求强制走主库,或者把对实时性要求最高的查询(比如登录、支付)路由到主库。
  • 从库负载不均衡。只读代理不一定能完美均摊流量,要随时监控从库的CPU和QPS,避免一个从库挂了把流量压回主库。

至于分库分表,我要劝一句:这是最后的手段,绝不能在项目初期就引入。分库分表会让所有表关联查询、聚合查询、跨库事务变成地狱级复杂度。我见过太多团队在日均几万流量时就做了分库分表,结果业务发展期疯狂接需求时,每天都要为跨库查询写补偿逻辑。

什么样的数据量才应该分库分表?我的经验阈值是:单表行数超过2000万,或者单库QPS持续超过2000,且读写分离和缓存都做过了仍然扛不住。只有到了这种情况,才值得付出运维和开发的巨大代价去拆分。

5.4 数据库连接池调优:最容易被低估的一步

上面已经提到了连接池的容量问题,这里再补充一下日常调优经验。HikariCP的常用配置项:

配置项推荐值说明
maximumPoolSize10~20(压测调整)最大活跃连接数
minimumIdle5~10最小空闲连接数,避免频繁创建新连接
connectionTimeout30000获取连接的超时时间,默认太大了,建议30秒以内
maxLifetime1800000连接最大生命周期,建议小于数据库wait_timeout,避免拿到已断开的连接
validationTimeout5000连接有效性检测超时,建议尽量短

很多人知道调maximumPoolSize,却不知道maxLifetime和wait_timeout的关系。MySQL默认的wait_timeout是8小时,如果一个连接空闲超过8小时会被MySQL主动断开,而连接池里的连接又没被及时检测到,那么下一次使用这个连接时就会直接抛Communications link failure。经典的"偶发性连接异常"问题,排查到最后往往就是这两个参数没匹配好。

6. 第五板斧:水平扩展与流量治理——从"扛得住"走向"扛不死"

单机和数据库的优化做完,CPU、内存、数据库都在合理负载范围内,但系统依然无法满足业务增长的话,就该考虑架构层面的水平扩展了。这个话题很大,我只挑面试中的高频考点和对我实际帮助最大的几个点展开。

6.1 无状态化:水平扩展的前提

水平扩展的核心假设是:任何一台机器都可以处理任何请求,或者说请求可以被均匀地分发到任意节点上,而结果一致。

但有状态的系统做不到这一点。比如你把用户的Session存在应用服务器的内存里,一台机器挂了,所有登录该机器的用户就都掉了。所以做集群前,必须把所有状态外置:

  • Session外置到Redis,或者用JWT之类无状态Token做认证。
  • 本地缓存要么不存写数据,要么做成"数据变更时主动失效"的机制(否则不同机器缓存里的数据不一样)。
  • 定时任务必须加分布式锁(基于Redis或数据库),否则同一个任务在多台机器上会重复执行。

"无状态化"这个词说出来很简单,但实际操作中,排查一个隐藏的状态依赖特别费劲。我曾经接手过一个老系统,明明是分布式部署,但代码里有一个static Map存了用户登录token,导致用户登录成功之后,再次发出的请求如果被负载均衡路由到另一台机器就会偶发退登。这种问题不是架构设计的问题,而是编码习惯的问题——所以无状态化不仅是一个部署架构概念,更是一个代码规范概念。

6.2 消息队列:削峰填谷,把"瞬时并发"变平滑

消息队列(Kafka、RocketMQ、RabbitMQ等)在并发性能提升中的作用,本质上是一个削峰填谷的过程:下游系统处理能力是固定的(比如每秒最多处理1000个请求),但当流量突发到每秒5000时,直接打进来必然导致系统被打爆。用消息队列把5000个请求快速写入队列,然后下游按自己的节奏(比如每秒800~1000)消费处理,系统能平稳度过流量高峰。

核心优势有三点:

  • 异步解耦:上游不需要等下游处理完,请求响应更快。
  • 削峰填谷:下游可以"以时间换容量",从容处理。
  • 流量缓冲:即使下游短暂不可用,消息也能在队列里堆积,等恢复后继续消费。

这个方案的代价是:实时性下降(消息有延迟)、引入额外组件和运维成本、以及消息队列本身也可能成为瓶颈。所以我一般只在两种场景下用MQ:第一,某个步骤本身就不要求实时完成(比如发通知、做统计);第二,流量有明显的尖峰(比如秒杀),需要缓冲。

6.3 限流、降级、熔断:并发能力不等于无限抗压

很多人对并发性能的理解是"能扛住多少QPS",但真实生产环境里,更重要的能力是在超出预估流量时能优雅退场,而不是让整个系统崩溃。这四个字经常被放在一起说,但它们的侧重点完全不同:

  • 限流:控制进入系统的请求速率。常用的算法有固定窗口、滑动窗口、漏桶、令牌桶。令牌桶最常用,因为允许一定程度的突发流量。落地工具可以用Guava的RateLimiter(单机)、Redis+Lua(分布式)。
  • 降级:当系统压力大时,主动牺牲一些次要功能(比如双十一时关闭商品评论展示),把资源让给核心链路。
  • 熔断:当某个下游服务持续异常时,不再发起调用,直接返回预设的降级结果,防止故障蔓延。经典实现是Hystrix和Resilience4j。

这三件事的底层逻辑都是预设好失败场景的决策机制,而不是任由故障在系统中自由扩散。我做过一次保命操作:某个数据库节点磁盘爆满,按常理所有依赖该库的接口都要受影响。但因为之前做了熔断配置,所有读操作的失败回退是"直接返回Redis缓存中最近一份可用数据",服务整体只降级了20%的查询准确性,主链路的订单查询完全没受影响。

6.4 压测验证与容量评估:优化结果到底怎么样

架构改造完成后,必须用压测数据说话。我的习惯是改造前后各做一轮完整压测,记录对比表:

指标优化前优化后提升比例
最大QPS8005200650%
RT均值180ms42ms77%
RT P99720ms110ms85%
错误率1.5%0.05%97%
CPU平均使用率85%45%47%

这份数据表,不仅是我评估自己优化效果的依据,也是我向团队和领导汇报成果时最有力的论据。面试时如果能够说出"优化前单机QPS是800,加了本地缓存和连接池调优后到了2500,再做读写分离到了4500",说服力远远强于空泛地说"我做过高并发优化"。

7. 我在并发优化中反复踩到的三个坑

最后分享几个我过去几年实际踩坑的心得,这些都不在教科书里,但每一个都花过我不少线上排查的时间。

第一,压测环境和生产环境差异过大,压测结果失真。最典型的例子就是压测时用的数据库是低配的,而生产库是高配的;压测机的网络是千兆内网,而生产有公网带宽限制。导致压测非常理想,上线后流量一大就出问题。现在我要求压测环境至少在数据库规格、网络带宽、JVM参数这三个维度上跟生产保持一致,否则压测数据只作为参考。

第二,只优化了"并发路径"却忽略了"异常路径"。并发优化的关注点几乎都在主链路上:查询有没有走索引、线程池够不够用、缓存击穿怎么办。但真正的高并发事故往往发生在异常路径上——比如某个接口超时重试机制设计不佳,导致一次上游超时触发了下游10次重试,把下游压垮了。限流和熔断不仅要在主链路上做,更要在所有重试、回调、异步任务上做。

第三,GC调优是"最后一公里"但也是最容易被忽略的。当你的应用线程本身很快时,GC停顿就成了唯一的长尾延迟来源。我在压测中发现过一个场景:并发300时P99突然从40ms飙到500ms,查了很久,最后用jstat发现是CMS的GC周期和压测的高峰时间高度重合。后来通过调整堆大小(把-Xmx和-XX:MaxGCPauseMillis合理配置)和改用G1,P99稳定在了60ms以内。这里给个实用建议:压测时一定要同时采集GC日志,这一步很多人的压测脚本里根本没包含。

面试被问"如何提升项目并发性能",本质上是一个"你有什么可说的"的问题。有完整的方法论框架、有每个方案背后的取舍判断、有真实踩坑的细节数据——这些东西组合起来,才是一个能让面试官眼前一亮的回答。每次被问到这个问题的时候,我建议你先冷静地问一句自己:"我现在知道自己系统的瓶颈在哪儿吗?"如果不知道,那答案永远是先测出瓶颈再谈优化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询