后端性能优化实战:一次被产品经理逼出来的技术升级

红黑树下乘凉
2026-01-03 18:57
阅读 507

凌晨一点半,深圳的夜空下只有我和服务器还没睡。窗外科技园的写字楼早已漆黑一片,而我还在死磕一个慢得像蜗牛的接口——这已经是本周第三次被产品总监在晨会上点名了。

我是深圳某三线互联网公司的后端技术负责人,说三线其实有点自嘲,毕竟在深圳这片腾讯、华为扎堆的地界,我们这种百来号人的公司确实排不上号。但麻雀虽小五脏俱全,我们做的是一款面向中小企业的 SaaS 产品,用户量不大不小,刚好卡在“不优化就会崩”的尴尬线上。

事情要从上个月说起。产品经理老王(对,就是那个总说“这个需求很简单”的老王)提了个看似人畜无害的需求:“咱们能不能把报表加载时间从 8 秒降到 2 秒以内?客户反馈太慢了。”当时我嘴上答应着“没问题”,心里却在翻白眼——那张报表背后是五个微服务、十几张表的联查,还有复杂的权限校验逻辑,能跑出来就不错了好吗!

但抱怨归抱怨,活儿还得干。毕竟去年双11期间因为类似问题导致服务雪崩的惨痛教训还历历在目,那次事故之后老板直接把我们的年终奖砍了一半。所以这次,我决定来次彻底的技术探索与实践优化。

从火焰图开始的救赎之路

首先得定位瓶颈。我祭出了老搭档:perf + FlameGraph。几行命令下去,一张火焰图赫然显示——60% 的 CPU 时间都耗在了 JSON 序列化上!原来是我们用的 Jackson 在处理嵌套对象时效率极低,尤其是当返回数据包含大量关联实体时。

// 旧代码,罪魁祸首
@RestController
public class ReportController {
    @GetMapping("/report")
    public ResponseEntity<ReportDTO> getReport(@RequestParam String userId) {
        // 复杂业务逻辑...
        return ResponseEntity.ok(reportService.generateReport(userId));
    }
}

// ReportDTO 里嵌套了 User, Company, Metrics 等多个对象

看到这里我差点一口老血喷出来——这不就是典型的“过度序列化”吗?很多字段前端根本不用,但我们一股脑全塞进 DTO 返回了。更离谱的是,有些字段还是懒加载的 Hibernate 代理对象,序列化时触发了 N+1 查询!

踩坑一:别让 ORM 和 JSON 序列化直接握手

解决方案其实很直接:按需返回字段。我们引入了 GraphQL 的思想,但没直接上 GraphQL(运维同学会杀了我),而是自己搞了个轻量级的字段过滤器:

@GetMapping("/report")
public ResponseEntity<String> getReport(
    @RequestParam String userId,
    @RequestParam(required = false) String fields // 前端指定需要的字段,如 "user.name,metrics.sales"
) {
    ReportData data = reportService.getRawData(userId);
    String json = fieldFilter.filter(data, fields); // 自定义过滤器
    return ResponseEntity.ok(json);
}

配合 Jackson 的 @JsonView,效果立竿见影。序列化时间从 3.2s 降到了 400ms。

缓存策略:从“有缓存就行”到“精准打击”

解决了序列化问题,火焰图又指向了数据库查询——特别是那个该死的 LEFT JOIN 五张表的语句。DBA 小李看到 SQL 语句后直接拍桌:“你们后端是不是对 LEFT JOIN 有什么误解?”

说实话,早期为了赶项目进度,我们确实写了不少“能跑就行”的 SQL。现在用户量上来后,这些债都得还。

踩坑二:缓存不是万能胶,贴哪都行

一开始我想简单粗暴地上 Redis 缓存整个报表结果。但很快发现问题:不同用户看到的数据完全不同(多租户 + RBAC 权限),缓存命中率低得可怜。而且报表数据实时性要求高,缓存过期策略很难把握。

后来灵机一动:分层缓存。把报表拆解成几个核心数据块:

数据块 更新频率 缓存策略
用户基础信息 Redis 永久缓存 + 主动更新
实时销售数据 本地缓存 (Caffeine) + 5秒过期
权限计算结果 Redis 10分钟过期

关键代码用了 Spring Cache 的组合注解:

@Service
public class ReportDataService {
    
    @Cacheable(value = "userProfile", key = "#userId")
    public UserProfile getUserProfile(String userId) {
        // 从 DB 加载,几乎不变
    }
    
    @Cacheable(value = "salesMetrics", key = "#tenantId", 
               cacheManager = "caffeineCacheManager") // 本地缓存
    public SalesMetrics getRealTimeSales(String tenantId) {
        // 高频查询,允许短暂延迟
    }
}

这一招下来,数据库 QPS 从 1200 降到了 300,主从延迟问题也缓解了。

异步化:当同步请求变成“历史包袱”

但最头疼的还不是这些。报表里有个“预测分析”模块,调用 Python 写的机器学习模型,单次推理就要 3-5 秒。之前是同步调用,直接拖垮了整个接口。

踩坑三:别让 I/O 密集型任务霸占你的 Tomcat 线程池

我试过增加 Tomcat 线程数,结果只是把 OOM 的时间推迟了几分钟。真正的解法是 异步非阻塞。但改造起来比想象中麻烦——前端要支持轮询或 WebSocket,后端要管理任务状态,还得考虑失败重试。

最终我们采用了“先返回骨架屏,后台计算完成后推送”的方案:

  1. 前端请求 /report/init → 立即返回基础数据 + taskId
  2. 前端用 taskId 轮询 /report/status
  3. 后端用 @Async 执行耗时任务,结果存入 Redis
  4. 状态变为 DONE 后,前端拉取完整数据
@RestController
public class AsyncReportController {
    
    @PostMapping("/report/init")
    public InitResponse initReport(@RequestBody ReportRequest req) {
        String taskId = UUID.randomUUID().toString();
        asyncReportService.generateAsync(taskId, req); // 异步执行
        return new InitResponse(taskId, getBasicData(req)); // 立即返回
    }
    
    @GetMapping("/report/status/{taskId}")
    public TaskStatus checkStatus(@PathVariable String taskId) {
        return taskService.getStatus(taskId); // 检查 Redis 中的状态
    }
}

虽然增加了前端复杂度,但用户体验反而更好了——至少不会对着转圈圈干等 8 秒。产品经理老王居然难得地说了句:“这次改得不错!”

监控闭环:没有度量就没有优化

所有优化做完后,我做了件更重要的事:建立性能基线监控。以前都是等用户投诉才行动,现在我们在 Grafana 上配置了关键接口的 P95 延迟告警,阈值设为 1.5 秒。

# prometheus.yml 片段
- job_name: 'backend-metrics'
  metrics_path: '/actuator/prometheus'
  static_configs:
    - targets: ['backend-service:8080']

每周团队站会,我都会展示这张图表:

日期 报表接口 P95 (ms) DB CPU (%) 缓存命中率 (%)
优化前 8200 78 42
优化后一周 1850 35 89
当前 1620 28 93

数据不会说谎。上周五上线后,我终于能在晚上十点前下班了(虽然是因为第二天要陪老婆产检,但也是进步)。

写在最后:技术优化的本质是平衡

回顾这次折腾,最大的感悟不是学到了多少新技术,而是明白了优化永远是在做权衡

  • 开发效率 vs 运行效率:分层缓存增加了代码复杂度,但换来的是系统稳定性
  • 实时性 vs 性能:异步化牺牲了“一次请求搞定”的简洁性,但避免了线程阻塞
  • 通用性 vs 专用性:自研字段过滤器不如 GraphQL 完善,但足够轻量且符合团队现状

在深圳这座内卷之都,作为三线公司的技术负责人,我深知我们没法像大厂那样投入海量资源做极致优化。但正是这种“资源有限”的约束,逼着我们思考哪些优化真正带来价值,而不是盲目追新。

下次再遇到产品经理说“这个需求很简单”时,我可能会笑着回一句:“简单,但得加钱——或者加时间。”

不过话说回来,看着监控曲线稳步下降,那种“搞定它”的成就感,大概就是支撑我们这群深夜码农继续肝下去的动力吧。

(完)


附:关键优化点速查表

问题领域 优化手段 效果 注意事项
JSON 序列化 字段按需返回 + JsonView 序列化时间 ↓ 87% 需要前后端约定字段规范
数据库查询 分层缓存策略 DB QPS ↓ 75% 注意缓存一致性
耗时计算 异步任务 + 轮询 主接口响应时间 ↓ 80% 增加前端复杂度
监控 Prometheus + Grafana 问题发现时间从天级到分钟级 需要定义合理告警阈值

P.S. 如果你在深圳,欢迎约咖啡聊聊分布式系统的那些坑——反正我凌晨两点前应该都醒着。

评论 0

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