多线程Spring Boot应用致MySQL进程睡眠状态的解决方法
我有一个Spring Boot定时任务应用,其中一项任务通过多线程异步执行,每个线程向兼容MySQL的AWS Aurora Serverless数据库插入约20万条数据。任务执行完成后,线程已经关闭,但MySQL中对应的进程仍处于睡眠状态,直到超时才会终止。
代码示例如下:
@SpringBootApplication @EnableScheduling public class ScheduledServiceApp implements SchedulingConfigurer { private static final int POOL_SIZE = 20; public static void main(String[] args) { SpringApplication.run(ScheduledServiceApp.class, args); } @Override public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(POOL_SIZE); threadPoolTaskScheduler.setThreadNamePrefix("scheduled-task-pool-"); threadPoolTaskScheduler.initialize(); scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler); } } @Component public class ScheduledTask { // ..... @Scheduled(cron = "${task.cron.expression}", zone="UTC") // @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional public void runTask() { try { // ..... ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(THREAD_COUNT); for (int i = 0; i < THREAD_COUNT; ++i) { Runnable task = new TaskThread(); executor.execute(task); } executor.shutdown(); while (!executor.awaitTermination(1, TimeUnit.SECONDS)) { // wait until the processes in the executor will be terminated } log.info("Task is completed successfully"); } catch (Exception e) { // log the error } } } public class TaskThread implements Runnable { // ..... public void run() { dataService.insertData(/* parameters */); } }
每个线程执行时长约30分钟,任务完成后MySQL控制台仍可见这些睡眠状态的进程。尝试过@Modifying注解但无效,我不想降低MySQL的超时时间以免影响直接在MySQL中运行的查询,希望找到能让任务结束后立即终止这些MySQL进程的方案。
补充说明:应用由容器托管,运行在Tomcat下,使用MySQL的JDBC/ODBC桥(兼容Aurora Serverless MySQL 5.7)。
1. 修正事务注解的作用范围
当前@Transactional注解加在runTask()方法上,但子线程不会继承父线程的事务上下文,这会导致父线程的事务迟迟无法提交,或者连接被错误持有。正确的做法是将事务注解移至实际执行数据插入的insertData()方法上,确保每个子线程的操作在独立事务中完成,执行完毕后立即释放数据库连接:
@Service public class DataService { @Transactional public void insertData(/* parameters */) { // 执行批量插入逻辑 } }
同时移除ScheduledTask.runTask()上的@Transactional注解,因为该方法仅负责调度子线程,不需要事务上下文。
2. 使用Spring托管的线程池替代临时线程池
每次任务执行时创建newFixedThreadPool会导致线程频繁创建销毁,且可能导致连接池中的连接无法正确回收。改用Spring管理的ThreadPoolTaskExecutor,统一管理线程资源:
首先在配置类中定义线程池:
@Configuration public class ThreadPoolConfig { @Bean(name = "taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(THREAD_COUNT); executor.setMaxPoolSize(THREAD_COUNT); executor.setThreadNamePrefix("data-insert-pool-"); executor.setWaitForTasksToCompleteOnShutdown(true); executor.initialize(); return executor; } }
然后在ScheduledTask中注入使用:
@Component public class ScheduledTask { @Autowired @Qualifier("taskExecutor") private ThreadPoolTaskExecutor taskExecutor; @Scheduled(cron = "${task.cron.expression}", zone="UTC") public void runTask() { try { // ..... List<CompletableFuture<Void>> futures = new ArrayList<>(); for (int i = 0; i < THREAD_COUNT; ++i) { futures.add(CompletableFuture.runAsync(new TaskThread(), taskExecutor)); } // 等待所有任务完成 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); log.info("Task is completed successfully"); } catch (Exception e) { // log the error } } }
3. 优化数据库连接池配置
通过调整连接池参数,确保空闲连接能被主动回收,而不是依赖MySQL的超时机制。以常用的HikariCP为例,在application.properties中添加以下配置:
# HikariCP配置 spring.datasource.hikari.max-lifetime=1800000 # 连接最大生命周期(30分钟,需小于MySQL的wait_timeout) spring.datasource.hikari.idle-timeout=600000 # 连接空闲10分钟后回收 spring.datasource.hikari.connection-timeout=30000 # 获取连接超时时间 spring.datasource.hikari.minimum-idle=5 # 最小空闲连接数 spring.datasource.hikari.maximum-pool-size=20 # 最大连接数
这里设置idle-timeout小于MySQL的默认wait_timeout(通常为8小时),让连接池主动回收长时间空闲的连接,避免MySQL端出现睡眠进程。
4. 确保批量插入后资源正确释放
如果使用JPA的EntityManager,在批量插入完成后手动清除上下文:
@Transactional public void insertData(/* parameters */) { // 批量插入逻辑 entityManager.flush(); entityManager.clear(); }
如果使用JDBC的Connection,务必使用try-with-resources确保连接关闭:
public void insertData(/* parameters */) { try (Connection conn = dataSource.getConnection()) { // 执行批量插入 } catch (SQLException e) { // 处理异常 } }
内容的提问来源于stack exchange,提问作者Armine

