Note 20. 弹性设计与并发:异步任务、定时调度与虚拟线程
Note 20. 弹性设计与并发:异步任务、定时调度与虚拟线程
ProriseNote 20. 弹性设计与并发:异步任务、定时调度与虚拟线程
摘要: 本章我们将深入 Spring Boot 的并发编程模型。我们将学习如何使用 @Async 将耗时操作异步化以提升接口响应速度,并掌握 @Scheduled 实现 Cron 表达式级别的定时任务。最重要的是,我们将配置 自定义线程池 以避免生产环境的内存溢出风险,并探索 Spring Boot 3.2+ 对 JDK 21 虚拟线程 的原生支持,体验高并发 IO 的新解法。
本章学习路径
- 异步处理:使用
@Async实现“火后即焚”的异步调用。 - 线程池优化:替换默认的危险线程池,配置生产级
ThreadPoolTaskExecutor。 - 定时调度:使用
@Scheduled实现固定频率与 Cron 表达式任务。 - 未来已来:在 Spring Boot 中启用 JDK 21 虚拟线程。
20.1. 异步任务:@Async
20.1.1. 痛点:同步执行的阻塞
假设 UserService.register() 方法包含以下步骤:
- 插入数据库 (10ms)
- 发送欢迎邮件 (2000ms, 依赖外部 SMTP 服务)
- 发送注册短信 (500ms, 依赖外部 API)
如果同步执行,用户点击“注册”后需要等待 2.5s 才能收到响应,体验极差。实际上,只要数据库插入成功,就可以立刻告诉用户“注册成功”,邮件和短信可以在后台慢慢发。
20.1.2. 开启异步支持
首先,需要在启动类或配置类上添加 @EnableAsync 注解。
文件路径: src/main/java/com/example/demo/DemoApplication.java
1 | // 开启异步注解支持 |
20.1.3. 编写异步方法
将耗时逻辑抽取到一个单独的方法,并加上 @Async。
文件路径: src/main/java/com/example/demo/service/NotificationService.java
1 |
|
调用方:
1 |
|
20.2. [关键] 配置自定义线程池
生产事故预警:Spring 默认使用的 @Async 线程池是 SimpleAsyncTaskExecutor。这个“线程池”非常坑爹,它 不复用线程,每来一个任务就创建一个新线程。如果瞬间涌入 1000 个请求,它就会创建 1000 个线程,直接导致服务器 OOM (内存溢出) 宕机。
我们必须配置一个生产级的线程池。
文件路径: src/main/java/com/example/demo/config/AsyncConfig.java
1 | package com.example.demo.config; |
20.3. 定时任务:@Scheduled
后端系统经常需要执行周期性任务。Spring Boot 内置了轻量级的调度器。
20.3.1. 开启调度
在启动类上添加 @EnableScheduling。
1 |
|
20.3.2. 任务定义
文件路径: src/main/java/com/example/demo/task/DailyTask.java
1 |
|
Cron 技巧:不要死记硬背 Cron 语法,直接使用在线生成器(如 cron.qqe2.com),生成后复制进来即可。
20.4. [前沿] JDK 21 虚拟线程 (Virtual Threads)
如果你的项目使用的是 JDK 21 且 Spring Boot 版本 >= 3.2,恭喜你,你拥有了处理高并发 IO 的核武器。
20.4.1. 什么是虚拟线程?
- 传统线程 (Platform Thread):与操作系统线程 1:1 绑定。创建昂贵,数量有限(几千个就顶天了)。遇到 IO 阻塞(如查数据库、调 API)时,CPU 只能干等,浪费资源。
- 虚拟线程 (Virtual Thread):由 JVM 管理的轻量级线程。创建极其廉价(百万级随便开)。遇到 IO 阻塞时,JVM 会自动挂起它,让底层 carrier 线程去干别的事。
20.4.2. 一键开启
在 Spring Boot 3.2+ 中,开启虚拟线程只需要一行配置。
文件路径: src/main/resources/application.yml
1 | spring: |
开启后:
- Tomcat 会自动使用虚拟线程来处理 HTTP 请求(哪怕有 10 万个并发请求,也不会把线程池撑爆)。
- @Async 如果没有指定自定义线程池,也会尝试使用虚拟线程执行任务(SimpleAsyncTaskExecutor 在 3.2+ 对虚拟线程做了优化)。
20.4.3. 适用场景
- 适合:IO 密集型任务(Web 服务、数据库操作、RPC 调用)。虚拟线程能把吞吐量提升数倍。
- 不适合:CPU 密集型任务(视频转码、复杂数学计算)。虚拟线程并没有增加 CPU 核心数,这时候还是得靠传统线程池。
20.5. 本章总结与并发速查
摘要回顾
本章我们让系统具备了“三头六臂”的能力。我们使用 @Async 将耗时任务剥离主线程,配置了安全的 ThreadPoolTaskExecutor 防止 OOM;使用 @Scheduled 实现了自动化运维任务;最后介绍了 JDK 21 的虚拟线程,为未来高并发架构指明了方向。
遇到以下 3 种并发场景时,请直接参考代码模版:
1. 场景一:发送通知/日志异步入库
需求:不阻塞主流程,后台执行。
方案:@Async + 自定义线程池。
代码:
1 |
|
2. 场景二:定期报表/状态同步
需求:每隔 10 分钟拉取一次支付状态。
方案:@Scheduled(cron = "0 0/10 * * * ?")。
3. 场景三:极高并发的 I/O 密集型 API
需求:类似于网关或聚合服务,需要同时调用几十个外部 API。
方案:升级 JDK 21 + Spring Boot 3.2,开启 spring.threads.virtual.enabled=true。
4. 核心避坑指南
异步失效 (自调用)
- 现象:
this.sendEmail()是同步执行的。 - 原因:AOP 代理失效(同
@Transactional)。 - 对策:注入自己或拆分 Service。
- 现象:
定时任务单线程阻塞
- 现象:定义了多个
@Scheduled任务,如果任务 A 卡住了,任务 B 居然也不执行了。 - 原因:Spring 默认的 Scheduling 线程池大小只有 1。
- 对策:在
application.yml配置spring.task.scheduling.pool.size=5。
- 现象:定义了多个
ThreadLocal 数据丢失
- 现象:在
@Async方法里调用UserContext.getUserId()拿不到数据。 - 原因:ThreadLocal 是线程绑定的,异步方法在 新线程 里执行,自然拿不到主线程的数据。
- 对策:在调用异步方法前,手动将数据作为参数传过去;或者使用
TransmittableThreadLocal(阿里开源工具) 来解决线程间上下文传递问题。
- 现象:在








