一次从“能用”到“好用”的性能优化实战

代码里的小宇宙
2025-06-27 08:25
阅读 310

大家好,我是某互联网公司的一名后端工程师,平时主要负责高并发服务的开发和维护。今天想和大家分享一个最近让我印象深刻的项目——我们是如何在一个关键链路上对一个日均请求过亿的服务进行深度性能优化的。

这篇文章不是为了炫技,也不只是为了说“我们搞定了”,更希望把整个过程中踩过的坑、学到的经验真实地呈现出来。如果你也在做类似的性能优化工作,或许可以少走一些弯路。


背景:为什么要做这次优化?

背景:为什么要做这次优化?

这个项目是公司核心推荐系统的下游服务之一,负责根据用户画像和行为数据,计算出一组匹配度最高的物品候选列表,传给上层排序模块。

服务上线时表现不错,但随着业务增长和新功能接入,响应时间逐渐变长,TP99在高峰期甚至突破了300ms。不仅影响了整体推荐链路的耗时,还导致调用方频繁超时熔断。问题已经严重影响体验和转化率。

我们决定启动专项优化,目标是将TP99控制在100ms以内,并提升服务的稳定性


遇到的挑战:不只是代码慢的问题

遇到的挑战:不只是代码慢的问题

接到任务的时候,我第一反应是看看接口耗时分布和堆栈信息,结果发现几个问题:

  • 方法级热点明显:部分SQL查询和规则逻辑非常耗时
  • GC压力陡增:JVM Full GC频率从每小时几次上升到了十几分钟一次
  • 线程池排队严重:异步任务调度经常卡住,资源争抢明显
  • 缓存命中率下降:新增的一些标签维度让Redis缓存利用率降低了近30%

这些问题交织在一起,很难简单归因为“某个SQL写得不好”。我们要做的,是一次全面的性能梳理和架构重构。


技术方案:不止于微调,而是系统性优化

技术方案:不止于微调,而是系统性优化

整体思路

我们的核心策略是:“先降负载,再提效率”,分为以下几个阶段:

  1. 降负载:减少冗余计算和无效请求
  2. 提速:提升单次处理效率
  3. 稳控:加强容灾和限流机制
  4. 监控:构建多维观测体系

关键技术选型与权衡

方向 方案 权衡说明
缓存优化 Caffeine + Redis组合本地+远程 避免全量加载,同时保持一致性
异步化处理 使用CompletableFuture重构逻辑 提升并行能力,降低阻塞风险
数据库 查询拆分+索引优化 避免大表扫描和锁竞争
JVM参数 G1GC + 调整MaxGCPauseMillis 平衡吞吐与延迟

具体实践与代码片段分享

具体实践与代码片段分享

下面我想重点讲几个对我们影响比较大的优化点,附上具体的实现思路和代码示例。

1. 缓存策略升级:Caffeine本地缓存+Redis双层缓存

原来的架构中,我们只用了Redis做集中式缓存,但在高并发场景下出现了两个问题:

  • 网络延迟累积
  • Redis连接数过高导致抖动

于是我们在服务内部加了一层Caffeine本地缓存,用于缓存短期内高频读取的数据。

private final Cache<String, List<Item>> localCache = Caffeine.newBuilder()
    .maximumSize(5000)
    .expireAfterWrite(60, TimeUnit.SECONDS)
    .build();

public List<Item> getItemListFromCache(String key) {
    return localCache.getIfPresent(key);
}

// 如果本地缓存未命中,则访问Redis
public void setToCache(String key, List<Item> items) {
    localCache.put(key, items);
    redisTemplate.opsForValue().set("item_" + key, items, 3, TimeUnit.MINUTES);
}

这样做之后,减少了大约60%的Redis穿透流量,同时也避免了网络延迟带来的波动。

2. 异步串行转并行:利用CompletableFuture编排多个任务

原始的实现是同步等待每个规则模块执行完毕,而实际上这些规则之间并没有强依赖关系。通过 CompletableFuture 重构后,我们实现了多个规则并行执行。

public Future<List<Item>> executeRuleAAsync(User user) {
    return executor.submit(() -> ruleA.execute(user));
}

public Future<List<Item>> executeRuleBAsync(User user) {
    return executor.submit(() -> ruleB.execute(user));
}

public List<Item> combineResult(User user) {
    CompletableFuture<List<Item>> futureA = CompletableFuture.supplyAsync(() -> ruleA.execute(user), executor);
    CompletableFuture<List<Item>> futureB = CompletableFuture.supplyAsync(() -> ruleB.execute(user), executor);


![技术原理图-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062708/428eb78e-740b-4858-8d6b-eba7ed150b95.jpg)


    return futureA.thenCombine(futureB, (listA, listB) -> {
        // 合并逻辑
        List<Item> combined = new ArrayList<>();
        combined.addAll(listA);
        combined.addAll(listB);
        return combined;
    }).join(); // 实际使用中建议封装成异步回调
}

技术应用场景-2

注意:线程池的大小需要根据CPU核数和实际压测结果调整,不能一味追求高并发。

3. SQL优化:拆查询、加索引、去重计算

我们原来的SQL是一个超长查询,包含几十个JOIN和子查询。优化策略包括:

  • 拆分成多个小查询,在应用层合并
  • 增加合适的索引(尤其针对where条件和order by字段)
  • 移除不必要的distinct和group by

例如:

原始查询:

SELECT * FROM items WHERE user_id IN (SELECT ...) ORDER BY score DESC LIMIT 50

改为:

-- 先查主ID
SELECT id FROM item_score WHERE user_id = ? ORDER BY score DESC LIMIT 100
-- 再根据ID拉取详细信息
SELECT * FROM items WHERE id IN (?)

这一步优化直接将数据库侧的平均查询时间从180ms降到了40ms左右。


踩过的坑:别以为改完就万事大吉

坑一:本地缓存过大,反而导致OOM

一开始我们设置了一个很大的最大容量(maxSize=50000),结果上线没多久就有节点出现OOM错误。经过分析发现主要是由于某些key对应的value结构太大,加上更新频繁,导致缓存占用爆炸。

解决方式:动态调整本地缓存大小,结合监控指标进行动态扩缩容,或者限制单个对象的最大size。

坑二:CompletableFuture的异常处理不完善

刚开始并行化改造时,没有对异常做兜底处理,导致一旦其中一个规则失败,整个Future链就会中断。

教训:务必在关键环节添加 .exceptionally() 或者 .handle() 回调,做好异常兜底和补偿机制。

坑三:线程池配置不合理引发线程饥饿

我们最开始统一使用一个共享线程池,结果在高峰期时,大量IO操作和CPU密集型任务混在一起,出现了严重的线程争抢。

改进:为不同类型的任务划分专用线程池,比如:

  • IO线程池(Redis/DB)
  • CPU密集型任务线程池(算法打分等)
  • 默认后台线程池

最终效果:从300ms → 75ms,稳定性和可用性也大幅提升

优化完成并灰度上线后,我们观察到:

  • TP99 从300ms下降至75ms
  • Full GC频率从每小时2~3次降至几乎每天只有1次
  • 整体QPS提升了约3倍
  • 调用方熔断次数接近清零
  • Redis连接数降低约60%
  • 日志级别统一规范后,问题定位效率显著提高

最重要的是,整个服务具备了更强的横向扩展能力,为后续业务增长留足了空间。


经验总结:性能优化这件事,到底靠什么?

这次优化经历让我深刻意识到,性能优化从来都不是简单的“哪里快改哪里”,而是要有一套完整的体系思维。

我总结了几点经验,供大家分享:

✅ 性能问题一定是“综合性”的,单一手段很难见效

不要指望仅靠SQL优化或增加缓存就能解决问题,往往是多个因素叠加作用的结果。必须建立全局视角,从数据流、调用链、资源瓶颈等多个维度去分析。

✅ 好的监控体系是你的眼睛和耳朵

如果没有埋点、指标采集和调用链追踪,你就是在黑盒环境下调试。强烈推荐引入APM工具(如SkyWalking、Pinpoint)辅助诊断。

✅ 代码层面的小改动,可能带来系统级别的大变化

比如一行日志打印,如果嵌在循环里面,可能就是性能黑洞。每一个细节都不能放过。

✅ 性能优化要尽早,越晚代价越高

我们在服务已经承载较大流量才来做这次优化,很多决策都不得不更加保守。如果初期就能考虑设计上的可扩展性,后面会轻松很多。

✅ 技术之外的协作也很重要

性能优化往往涉及多个团队协同推进,比如前端同学配合做懒加载、DBA协助建索引等。跨部门沟通顺畅与否,直接影响项目节奏。


结语:优化永无止境,成长不止于眼前

回头看这次优化,虽然最终取得了不错的成果,但也留下了不少遗憾和思考。比如是不是还可以做得更极致?有没有更好的架构来支撑未来的扩容需求?

其实每一次优化,都是一次重新认识自己的过程。你可能会焦虑、疲惫、怀疑自己,但只要坚持下去,一定会有所收获。

如果你现在正面临类似的性能问题,不妨尝试从这几个方向入手:

  1. 构建可观测性体系(日志+指标+调用链)
  2. 定位瓶颈,优先处理“性价比”最高的点
  3. 做好压测和灰度,验证每一项改动的效果
  4. 多维度评估,不仅仅是速度,还有稳定性、可维护性和成本

愿你在性能优化的路上,越走越稳,越走越远。

—— 一位在深夜debug的后端工程师

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝