Spring Boot实战:从踩坑到稳如老狗的两年心路

技术边角料
2025-12-21 13:28
阅读 3838

大家好,我是小陈,目前在某电商中台团队做后端开发。别看我现在写得头头是道,其实刚入职那会儿还是个“Spring Boot Hello World”都要 Google 三遍的菜鸟。更惨的是,我是在试用期第三周就被丢进一个核心订单服务重构项目里——对,就是那种“你先试试,不行再说”的经典开局。

不过嘛,时间过得真快,一晃快两年了。这两年里,我从 Mac 上敲第一行 @SpringBootApplication 开始,经历了无数次凌晨三点的线上报警、被产品经理追着问“为什么接口又超时了”、以及运维大哥一句“这 Pod 又 OOM 了,你看看?”的日常。今天这篇就来聊聊我在 Spring Boot 实战中摸爬滚打出来的一些经验,不讲理论大道理,全是血泪教训和能直接抄的配置。


被逼出来的技术成长

去年双11前一个月,团队接了个硬核任务:把原来单体架构的订单模块拆成微服务,还必须用 Spring Boot + Kubernetes 部署。领导说:“你不是熟悉 K8s 吗?正好练练。”
我当时心里一万个草泥马奔腾而过——我确实玩过 K8s,但那都是本地 Minikube 玩玩,哪见过生产环境上万 QPS 的流量?

更要命的是,公司开发主力是 Windows,但我这人死脑筋,非得用 Mac 写代码(理由很简单:终端好看、命令行顺手)。结果每次联调都要在 Windows 虚拟机里跑一遍,测完再切回 Mac 改代码,效率低到想哭。测试同事都调侃我:“小陈,你这是跨平台开发,还是跨次元作战?”

但吐槽归吐槽,活还得干。于是,这场 Spring Boot 实战之旅,就在 deadline 倒计时 30 天的焦虑中开始了。


第一个坑:配置文件乱成一锅粥

刚开始我以为 Spring Boot 的 application.yml 很简单,dev/test/prod 三个 profile 搞定。结果上线第一天,测试环境连不上数据库——因为我在 application-prod.yml 里写了正确的 DB 地址,但 Jenkins 构建脚本默认加载的是 application-dev.yml!

后来才知道,Spring Boot 的配置加载顺序比我想的复杂得多。官方文档那张图我看了五遍才搞明白:

1. Devtools global settings (开发时)
2. @TestPropertySource / @SpringBootTest(properties)
3. 命令行参数 (--spring.profiles.active=prod)
4. SPRING_APPLICATION_JSON
5. ServletConfig / ServletContext 初始化参数
6. JNDI 属性
7. Java System.getProperties()
8. OS 环境变量
9. RandomValuePropertySource (random.*)
10. 打包 jar 内部的 application-{profile}.yml
11. 打包 jar 外部的 application-{profile}.yml

教训:永远不要依赖默认 profile!我们在 K8s Deployment 里显式指定:

env:
  - name: SPRING_PROFILES_ACTIVE
    value: "prod"

同时,所有敏感配置(DB 密码、AK/SK)全部交给 Vault 或 K8s Secret 管理,绝不硬编码。现在每次提 PR,CI 流水线都会扫一遍有没有明文密码,扫到直接 block —— 这招是从一次 P0 级事故中学来的,那次我把测试账号密码提交到了 GitLab 公共仓库……(别问,问就是黑历史)


日志与监控:别等线上炸了才想起看日志

早期我们的服务日志就是 System.out.println("来了!"),结果线上出问题时,日志里全是“来了!”、“走了!”、“又来了!”,根本没法排查。

后来痛定思痛,全面接入 ELK(Elasticsearch + Logstash + Kibana),并统一日志格式:

@RestController
@Slf4j // Lombok 注解,自动生成 logger
public class OrderController {

    @PostMapping("/create")
    public ResponseEntity<?> createOrder(@RequestBody OrderRequest req) {
        String traceId = UUID.randomUUID().toString();
        log.info("traceId={}, 创建订单请求, userId={}, items={}", 
                 traceId, req.getUserId(), req.getItems());
        
        try {
            Order order = orderService.create(req);
            log.info("traceId={}, 订单创建成功, orderId={}", traceId, order.getId());
            return ResponseEntity.ok(order);
        } catch (Exception e) {
            log.error("traceId={}, 创建订单失败", traceId, e); // 带异常堆栈
            return ResponseEntity.status(500).build();
        }
    }
}

关键点:

  • 每个请求生成唯一 traceId,贯穿整个调用链
  • 日志必须包含业务关键字段(userId、orderId)
  • 错误日志必须带 e,否则 Kibana 里看不到堆栈

配合 Prometheus + Grafana,我们还暴露了 Spring Boot Actuator 的指标:

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

现在运维大哥再也不用半夜打电话问我“你服务挂没挂”,直接看 Grafana 面板就知道 CPU、内存、GC、HTTP 5xx 的趋势。上周五晚上我还靠这个提前发现了数据库连接池泄漏,避免了一次双11预演事故——那一刻,我觉得自己终于从“背锅侠”进化成了“预警侠”。


性能优化:别让 Spring Boot 成为慢羊羊

有次压测,QPS 刚到 500 就开始大量超时。Profiler 一看,好家伙,90% 的时间花在 ObjectMapper 序列化上!原因是我们每个接口都手动 new 了一个 ObjectMapper,而它内部有复杂的缓存初始化逻辑。

解决方案:全局复用!

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        return mapper;
    }
}

另外,数据库查询也是重灾区。早期代码里充斥着:

List<Order> orders = orderRepository.findAll(); // 十万条数据全拉出来!
return orders.stream().filter(o -> o.getStatus() == PAID).collect(...);

这种 N+1 查询和内存过滤,在 K8s 里直接导致 Pod 内存飙升被 OOMKill。后来强制推行:

  • 所有查询必须走索引
  • 分页查询上限 100 条
  • 复杂聚合用 MyBatis Plus 的 QueryWrapper 或原生 SQL

附上我们优化前后的对比数据:

指标 优化前 优化后 提升
平均响应时间 420ms 85ms 4.9x
P99 延迟 1200ms 210ms 5.7x
内存占用 (Pod) 1.2GB 480MB 60% ↓
GC 频率 15次/分钟 2次/分钟 86% ↓

云原生适配:让 Spring Boot 在 K8s 里舒服地跑

很多人以为 Spring Boot 打个 jar 包扔进容器就完事了,其实不然。我们踩过几个典型坑:

1. 健康检查不兼容

Spring Boot 默认的 /actuator/health 返回 200 就算健康,但 K8s 的 liveness probe 如果太敏感,会导致服务刚启动就被 kill。

做法:

  • readinessProbe 检查 /actuator/health/readiness
  • livenessProbe 检查 /actuator/health/liveness
  • 初始延迟设长一点(比如 30s),给应用留足启动时间

2. 资源限制没配,被调度器制裁

一开始我们没设 CPU/Memory limit,结果某个服务内存暴涨,把同节点其他 Pod 全挤出去了。后来严格按以下原则配置:

resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "1Gi"
    cpu: "1000m"

并通过 -XX:+UseContainerSupport 让 JVM 感知容器内存限制(Java 8u191+ 默认开启)。

3. 优雅停机没开,请求被中断

用户支付成功,结果服务滚动更新时进程被 kill,回调没处理——钱付了但订单没生成。恐怖如斯!

解决:开启优雅停机

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

同时 K8s 的 terminationGracePeriodSeconds 设为 45s,确保有足够时间处理完存量请求。


最后一点真心话

写这篇文章的时候,我刚通过转正答辩(没错,试用期真的把我虐哭了)。回头看这两年,Spring Boot 对我来说早已不是“框架”,而是每天打交道的“同事”。它有时候很贴心(自动配置省掉 80% 样板代码),有时候也很傲娇(版本升级一堆 breaking change)。

但正是这些实战中的坑、半夜的报警、和队友一起 debug 到天亮的经历,让我真正理解了什么叫“工程能力”。技术没有银弹,只有不断试错、复盘、沉淀。

如果你也在试用期焦虑,或者刚接手一个烂摊子项目——别慌。记住:每一个稳如泰山的老狗,都曾是瑟瑟发抖的菜鸟。只要肯折腾,Spring Boot 会是你最可靠的战友。

对了,我们组下周还要重构支付回调模块,用 Spring Boot + Kafka + Saga 模式做分布式事务。要是你有相关经验,评论区聊聊?说不定能帮我避开新坑 😅


P.S. 本文所有配置和代码片段都来自我们生产环境(脱敏后),可以直接参考。但别照搬!每家公司网络策略、中间件版本、合规要求都不一样,务必结合自身情况调整。毕竟,别人的最佳实践,可能是你的生产事故。

评论 0

最热最新
暂无评论
技术边角料Lv.1
0
影响力
0
文章
0
粉丝