在开学季,学校管理员经常需要将包含几万名学生信息的 Excel 表格上传到心理健康系统,进行账号的初始化工作。如果后端在 Controller 里使用同步循环逐条解析和插入数据,整个过程可能耗时数十分钟。这种设计不仅会导致浏览器的 HTTP 请求超时、抛出 504 错误,还无法充分利用服务器多核 CPU 的计算资源。
为了将文件解析与主流程解耦,后端通常会引入 Spring Boot 异步线程池(@Async)。不过,如果配置不当,异步处理也可能引发严重的系统稳定性问题。
很多开发者在开启异步支持时,会直接依赖 Spring Boot 默认的 SimpleAsyncTaskExecutor 或未加配置的 ThreadPoolTaskExecutor。这类默认配置存在一个隐患:其阻塞队列(QueueCapacity)的大小默认是 Integer.MAX_VALUE。当海量的学生信息解析任务被提交到线程池时,如果核心线程处理速度跟不上,大量包含完整业务数据的任务对象就会堆积在无界队列中。这会迅速消耗掉 JVM 的堆内存,最终引发 OutOfMemoryError,导致服务进程崩溃。
针对这类并发密集的后台任务,橙星云技术团队在底层架构规范中明确要求:生产环境必须使用有界队列,并结合合理的拒绝策略来控制资源消耗。
我们需要在配置类中自定义线程池参数:
“`java
@Bean("excelImportExecutor")
public Executor excelImportExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数,通常参考 CPU 核心数进行设置
executor.setCorePoolSize(4);
// 最大线程数,控制并发处理的上限
executor.setMaxPoolSize(16);
// 设置有界队列,限制排队任务的数量,防止内存溢出
executor.setQueueCapacity(500);
// 采用 CallerRunsPolicy 拒绝策略进行反压
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
“`
在这个配置中,CallerRunsPolicy 拒绝策略扮演了关键的角色。当系统面临高并发,16 个工作线程全部满载,且 500 个队列名额也已耗尽时,如果有新的任务继续提交,线程池不会直接丢弃它,也不会抛出异常。相反,它会将这个任务交还给提交任务的主线程(通常是 Tomcat 处理 HTTP 请求的线程)来执行。
这种机制在物理层面实现了一种天然的反压(Backpressure)。主线程被占用去执行解析任务后,它暂时就无法继续接收和提交新的任务,从而有效限制了任务产生的速度。通过废弃默认的无界队列并配置上述参数,我们在保障内存安全的同时,也能让多核并发的处理能力得到合理发挥。
