技术探索与实践实践总结

索引没建好
2025-12-13 02:58
阅读 306

哈喽大家好,我是深圳某985计算机专业大三狗,目前在一家本地小厂实习(对,不是腾讯,但离腾讯滨海大厦骑个共享单车也就15分钟),秋招投简历投到手软,每天刷LeetCode的同时还得赶项目进度。最近刚搞完一个性能优化需求,被线上慢查询折磨得差点想转行送外卖,但最后还是啃下来了,今天就来水一篇技术复盘,顺便也给自己攒点面试素材——毕竟现在大厂面试题动不动就问“你做过哪些性能优化?”


为什么我要折腾这个?

事情起源于上周五晚上,产品哥突然在群里@我:“双11快到了,咱们首页加载速度再不提上来,老板要砍人了。”
我当时正啃着楼下肠粉店最后一份牛腩粉,看到消息差点把筷子扔了。

我们系统是个典型的电商导购页,后端用Spring Boot + MyBatis,前端Vue3。首页聚合了商品推荐、活动入口、用户行为日志上报等七八个模块,每个模块背后都是独立的微服务。之前跑得好好的,结果最近用户量涨了一波,QPS从200飙到1500+,首页P99响应时间直接干到2.8s,页面白屏半天,测试小姐姐天天追着我问“你们后端是不是又写了个 O(n²) 循环?”

说实话,当时真的想砸电脑。但冷静下来一想:这不就是绝佳的性能优化实战机会吗?而且这种问题特别适合拿来当综合型面试题——既考系统设计,又看底层细节,还能体现工程思维。


初步排查:先别急着改代码

很多人一听到慢,第一反应是“是不是SQL写得烂?”、“是不是没加缓存?”。但老鸟都知道,优化前必须先测量。于是我在测试环境搭了一套监控组合拳:

  • Arthas 看方法耗时
  • SkyWalking 追踪链路
  • Prometheus + Grafana 监控JVM和DB指标
  • MySQL slow log 开启阈值 200ms

一顿操作猛如虎,发现瓶颈其实不在SQL(虽然也有几条慢查,但影响不大),而是在接口聚合逻辑上。

具体来说,首页需要并行调用6个下游服务(用户画像、商品推荐、活动配置、AB实验、风控策略、埋点预热),但我们的代码是这样写的(简化版):

public HomePageDTO buildHomePage(UserContext ctx) {
    UserProfile profile = userProfileService.get(ctx.userId);
    List<Product> recs = recommendService.getRecommendations(ctx);
    ActivityConfig act = activityService.getConfig();
    AbTestResult ab = abTestService.eval(ctx);
    RiskCheckResult risk = riskService.check(ctx);
    PreloadData preload = preloadService.fetch(ctx);
    
    return new HomePageDTO(profile, recs, act, ab, risk, preload);
}

看起来没问题?错!这是串行调用!哪怕每个服务平均100ms,总耗时也要600ms起步。更别说网络抖动、GC停顿这些“惊喜”。

运维大哥看了直摇头:“你们后端是不是以为自己在写单机程序?”


改造方案:异步 + 缓存 + 容错

第一步:并行化调用

最直接的解法当然是异步并发。Java里用 CompletableFuture 走起:

public HomePageDTO buildHomePageAsync(UserContext ctx) {
    CompletableFuture<UserProfile> profileFut = 
        CompletableFuture.supplyAsync(() -> userProfileService.get(ctx.userId), executor);
    CompletableFuture<List<Product>> recsFut = 
        CompletableFuture.supplyAsync(() -> recommendService.getRecommendations(ctx), executor);
    // ... 其他5个类似

    return new HomePageDTO(
        profileFut.join(),
        recsFut.join(),
        // ...
    );
}

但这里有个坑:线程池不能随便用 ForkJoinPool.commonPool(),否则高并发下会阻塞整个应用。我专门配了一个自定义线程池:

# application.yml
thread-pool:
  home-page:
    core-size: 20
    max-size: 50
    queue-capacity: 100

实测下来,P99从2.8s降到800ms,效果立竿见影。

第二步:引入多级缓存

但还不够。比如“活动配置”这种数据,一天才变一次,完全没必要每次请求都去拉。于是我搞了个二级缓存策略

  • L1:Caffeine(本地缓存,TTL 5min)
  • L2:Redis(分布式缓存,TTL 30min)

伪代码如下:

public ActivityConfig getConfigCached() {
    return caffeineCache.get("act_config", key -> {
        ActivityConfig fromRedis = redisTemplate.opsForValue().get("act:config");
        if (fromRedis != null) return fromRedis;
        // 双检锁防止缓存击穿
        synchronized (this) {
            fromRedis = redisTemplate.opsForValue().get("act:config");
            if (fromRedis != null) return fromRedis;
            ActivityConfig fresh = activityService.fetchFromDB();
            redisTemplate.opsForValue().set("act:config", fresh, Duration.ofMinutes(30));
            return fresh;
        }
    });
}

吐槽一句:产品经理说“活动配置可能随时改”,结果上线一周就改了两次……早知道直接本地缓存10分钟都够了。

加上缓存后,P99进一步压到450ms

第三步:降级与熔断

但线上永远有意外。上周三凌晨2点,推荐服务挂了,导致我们首页直接500。运维半夜打电话骂我:“你这接口怎么连个兜底都没有?”

于是赶紧补上 Hystrix(或 Resilience4j)熔断 + 本地兜底数据

@HystrixCommand(fallbackMethod = "getDefaultRecommendations")
public List<Product> getRecommendations(UserContext ctx) {
    return recommendFeignClient.get(ctx);
}

public List<Product> getDefaultRecommendations(UserContext ctx) {
    // 返回静态热门商品
    return defaultRecs;
}

现在就算某个下游挂了,首页也能正常展示,只是少了个性化推荐——总比白屏强。


效果对比 & 数据说话

优化前后关键指标对比如下:

指标 优化前 优化后 提升
P99 响应时间 2800ms 420ms 85%↓
CPU 使用率 75% 45% 40%↓
错误率(5xx) 1.2% 0.03% 97%↓
DB QPS 3200 1800 44%↓

上线那天,测试小姐姐终于没来找我茬,反而发了个红包说“今晚请你喝喜茶”。(虽然最后是我请的,但情绪价值拉满了好吗!)


开发心得 & 面试能吹的点

这次折腾让我深刻体会到几个开发心得

  1. 不要过早优化,但一定要可测量。没有监控的优化都是耍流氓。
  2. 异步不是银弹。线程池配置、异常传播、上下文丢失(比如 MDC 日志)都要考虑。
  3. 缓存一致性比你想象中难。我们后来还加了 Redis Pub/Sub 做主动失效,不然运营改完配置要等半小时才生效,差点被投诉。
  4. 容错机制是线上稳定的底线。宁可少功能,不能崩主流程。

这些经验现在都成了我简历上的亮点,上周面腾讯光子工作室的时候,面试官正好问:“如果首页聚合多个服务,你怎么保证性能和可用性?” 我直接掏出这套方案,聊了半小时,最后他说“你这思路很清晰,符合我们对后端工程师的要求”。


写在最后

其实很多同学觉得“性能优化”很高大上,但真做起来就是抠细节 + 测数据 + 快速试错。我一开始也懵,但硬着头皮查日志、画链路图、压测对比,慢慢就摸出门道了。

秋招压力山大,但每次搞定一个线上难题,那种成就感真的能抵消掉投简历被拒的emo。技术这东西,实践出真知,光背八股文是不够的。

对了,如果你也在准备秋招,建议找个小项目动手优化一下——哪怕只是给自己的博客加个缓存,写进简历也比“熟悉Redis”有力得多。

好了,肠粉凉了,我去干饭了。下次再分享怎么用 Arthas 在生产环境抓死锁(又是血泪史……)。


P.S. 本文所有数据均为脱敏后的真实项目指标,技术栈基于 Spring Cloud Alibaba 生态。如有雷同,纯属大厂标配 😅

评论 0

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