从传统行业杀进码农圈:我在SpringBoot里和“资源”死磕的那些日子

梦想起航
2025-12-17 10:06
阅读 2267

大家好,我是老张(真名就不说了,怕被前同事认出来)。30岁高龄、去年刚从传统制造业转行做Java开发的“新人”。坐标成都,每天骑共享单车上下班,听着City Pop写代码,生活节奏舒服得有点不像搞IT的。说真的,要不是前公司连ERP系统都还在用VB6,我可能这辈子都不会想到自己会坐在这儿敲SpringBoot。

今天想跟大家聊聊一个让我头秃又成长飞快的经历——关于资源管理在SpringBoot项目中的实战探索。别误会,我说的不是云计算那种“资源”,而是更接地气的东西:文件、数据库连接、线程池、缓存……这些一旦没管好,轻则内存泄漏,重则服务直接崩盘。


起因:产品经理的一句“很简单”

事情发生在上个月,我们团队接了个内部工具重构任务。老系统是外包写的,代码像意大利面条,而且每次上传个Excel文件,服务器内存就蹭蹭涨,重启才能解决。运维小哥已经快把我拉黑了:“老张,你们这服务比我家猫还娇气。”

产品经理拍着胸脯说:“这个需求很简单,就是让用户上传一个CSV,解析后存到库里,再导出一份校验报告。”
我心想:不就是读文件、处理数据、写文件嘛?能有多难?

结果,第一次压测就翻车了。100个并发用户同时上传10MB的CSV,服务直接OOM(OutOfMemoryError),日志里全是:

java.lang.OutOfMemoryError: Java heap space

我当时盯着屏幕,差点把键盘砸了——这才入职半年,就要背锅线上事故了?


问题在哪?“资源”没关!

冷静下来一查,发现罪魁祸首是资源泄漏

老代码里,读取文件用的是FileInputStream,但根本没调用close();数据库连接用完也没归还;临时生成的报告文件堆积在/tmp目录,几天就占了几十GB。整个系统就像个漏水的桶,越跑越慢,最后直接干裂。

更扎心的是,我一开始居然没意识到这是“资源管理”问题。毕竟以前在工厂做PLC编程,资源?那是什么?有电就行!

但现实狠狠打了我一巴掌:在Java世界里,资源不是“用完就扔”的一次性筷子,而是需要你亲手“送回家”的孩子。


SpringBoot里的资源管理:优雅 vs 地狱

SpringBoot号称“约定优于配置”,但对资源管理这块,它只提供了工具,没替你兜底。你要是不用心,照样掉坑。

坑1:文件流没关,内存直接爆炸

最初我这么写:

public void parseCsv(String filePath) {
    FileInputStream fis = new FileInputStream(filePath);
    BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
    // ... 读取逻辑
}

看起来没问题?错!如果中间抛异常,fisreader永远不会关闭,文件描述符一直占用,最终导致系统无法打开新文件(Too many open files)。

解决方案:try-with-resources

SpringBoot项目里,一定要用JDK7+的try-with-resources语法:

public void parseCsv(String filePath) {
    try (FileInputStream fis = new FileInputStream(filePath);
         BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
        String line;
        while ((line = reader.readLine()) != null) {
            // 处理每一行
        }
    } catch (IOException e) {
        log.error("解析CSV失败", e);
        throw new BusinessException("文件读取异常");
    }
}

这样,无论是否抛异常,流都会自动关闭。血泪教训啊兄弟们!


坑2:临时文件堆积如山

我们导出的校验报告是临时文件,原代码直接扔到/tmp

File report = new File("/tmp/report_" + System.currentTimeMillis() + ".csv");
// ... 写入内容

但没人删!结果三天后,磁盘爆满,服务起不来。

解决方案:主动清理 + 使用NIO.2

现在我改用Files.createTempFile,并配合@PreDestroy或定时任务清理:

@Service
public class ReportService {

    private final List<Path> tempFiles = Collections.synchronizedList(new ArrayList<>());

    public Path generateReport(List<Data> data) throws IOException {
        Path tempFile = Files.createTempFile("validate_report_", ".csv");
        tempFiles.add(tempFile); // 记录,方便后续清理

        try (BufferedWriter writer = Files.newBufferedWriter(tempFile)) {
            // 写入数据
        }
        return tempFile;
    }

    @PreDestroy
    public void cleanup() {
        temp_files.forEach(path -> {
            try {
                Files.deleteIfExists(path);
            } catch (IOException e) {
                log.warn("清理临时文件失败: {}", path, e);
            }
        });
    }
}

虽然@PreDestroy不能100%保证执行(比如kill -9),但至少优雅停机时能清理。另外,我们还加了个每日凌晨的定时任务,扫一遍/tmp里超过24小时的文件。


坑3:数据库连接池被耗尽

有一次测试环境突然所有接口500,查日志发现:

HikariPool-1 - Connection is not available, request timed out after 30000ms.

原来是某个批量导入接口,开了事务但没控制批次大小,一次查10万条数据,连接一直占着不放。

解决方案:分页 + 显式控制事务边界

@Transactional
public void importBatch(List<Record> records) {
    int batchSize = 1000;
    for (int i = 0; i < records.size(); i += batchSize) {
        List<Record> batch = records.subList(i, Math.min(i + batchSize, records.size()));
        processBatch(batch);
        // 手动flush and clear if using JPA/Hibernate
        entityManager.flush();
        entityManager.clear();
    }
}

同时,在application.yml里合理配置Hikari连接池:

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      leak-detection-threshold: 60000 # 检测连接泄漏,单位ms

开启leak-detection-threshold后,如果连接借出去超过60秒没还,Hikari会打印堆栈,直接定位到问题代码。


资源监控:别等炸了才看

光靠代码规范不够,还得有监控兜底。我们团队后来做了三件事:

  1. Prometheus + Grafana 监控文件描述符和内存
  2. Arthas 在线诊断资源占用
  3. SonarQube 扫描资源未关闭的代码

比如用Arthas查线程:

thread -n 5  # 看CPU最高的5个线程
thread --state WAITING  # 看等待中的线程,可能卡在IO

或者查对象:

sc *FileInputStream  # 查类
getstatic java.io.FileInputStream$fd  # 不太准,但能辅助判断

虽然有点黑科技,但在救火时真香。


最终效果:从“救火队员”到“架构关注者”

经过这一轮折腾,我们的服务稳定性肉眼可见地提升:

指标 优化前 优化后
平均内存占用 1.2GB 600MB
文件描述符峰值 8000+ <500
月度OOM次数 3~5次 0次
临时文件堆积 几十GB/周 <100MB

最开心的是,上周五上线后,运维小哥居然在群里@我说:“老张,你们这服务最近稳得很啊!” —— 这句话比涨工资还爽。


写给和我一样的“转行新人”

说实话,刚转行那会儿,我对“资源”这种概念很模糊。总觉得SpringBoot自动配置啥都搞定了,自己只要写业务逻辑就行。结果现实教我做人。

但正是这些踩坑经历,让我开始真正关注架构设计代码质量。现在写代码,我会下意识问自己:

  • 这个流关了吗?
  • 这个连接会泄漏吗?
  • 这个临时文件谁来删?
  • 如果并发1000,会不会崩?

这种思维转变,比学会某个框架更重要。

如果你也是半路出家的程序员,别怕犯错。每个OOM背后,都藏着一次成长的机会。就像成都的茶馆,看似悠闲,其实每一片茶叶都经历过高温炒制——代码也一样,只有被Bug锤炼过,才能写出稳健的系统。

对了,我现在边听《Plastic Love》边写这篇博客。窗外玉林路的小酒馆还没打烊,而我的SpringBoot应用,正安静地运行在阿里云上,没有一丝内存波动。

真好。


附:资源管理Checklist(自用版)

  • 所有InputStream/OutputStreamtry-with-resources
  • 临时文件记录路径,程序退出或定时清理
  • 数据库操作控制批次大小,避免长事务
  • 连接池参数合理配置,开启泄漏检测
  • 线程池使用ThreadPoolTaskExecutor,拒绝策略明确
  • 大文件处理用流式(streaming),别全load进内存
  • 定期用jstatjmap、Arthas检查资源状态

共勉。

评论 0

最热最新
暂无评论
梦想起航Lv.1
0
影响力
0
文章
0
粉丝