Spring Boot 异步执行 详细教程
一、异步执行概述
想象一下你去奶茶店买奶茶:
- 同步模式:你点单后,必须站在柜台前等奶茶做好(奶茶师做完一杯再做下一杯),期间不能干其他事。
- 异步模式:你点单后,奶茶店给你一个取餐号(发起任务),你可以去旁边买零食(继续做其他事),等奶茶做好后店员叫号(通过回调或通知获取结果)。
Spring Boot异步执行的本质就是:让程序在执行耗时操作(如发邮件、调接口)时,不阻塞主线程,提升系统“同时处理多件事”的能力,让用户感觉更快。
二、异步执行的类型
Spring Boot中实现异步主要有2种方式,就像奶茶店的两种取餐方式:
类型 | 特点 | 适合场景 |
---|---|---|
@Async注解 | 简单粗暴,给方法加个注解就变异步(像“取餐号”) | 不需要返回结果的耗时操作 |
CompletableFuture | 能“链式”处理异步任务(像“边买零食边看手机,奶茶做好了短信通知你”) | 需要返回结果或组合多个异步任务 |
三、异步执行的使用场景
什么时候用异步?记住一个原则:“不着急要结果的耗时操作”。比如:
- 发短信/邮件(用户不需要立刻看到“已发送”)
- 记录日志(主业务完成后再慢慢写日志)
- 批量数据导入(比如导入10万条Excel数据,后台慢慢处理)
- 调用第三方接口(比如查物流,等它返回太慢)
- 下载文件
四、代码实现
方式1:@Async 注解
步骤1:开启异步支持
在启动类(XXXApplication.java
)上加注解 @EnableAsync
,告诉Spring“我要开始用异步了”。
@SpringBootApplication
@EnableAsync // 开启异步功能
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
步骤2:写一个异步方法
新建一个Service类(比如AsyncService
),里面写一个带@Async
注解的方法。
@Service
public class AsyncService {
// @Async:标记这个方法是异步的(默认用Spring内置线程池)
@Async
public void sendEmail(String userEmail) {
try {
// 模拟发邮件耗时2秒
Thread.sleep(2000);
System.out.println("邮件已发送到:" + userEmail);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
步骤3:调用异步方法
在Controller里注入AsyncService
,调用sendEmail
方法。
@RestController
public class UserController {
@Autowired
private AsyncService asyncService;
@GetMapping("/register")
public String register(String email) {
// 调用异步方法(不会等邮件发完,直接返回)
asyncService.sendEmail(email);
return "注册成功!邮件正在发送...";
}
}
测试效果:
启动项目,访问 http://localhost:8080/register?email=test@example.com
,页面会立刻返回**“注册成功”**,2秒后控制台打印“邮件已发送”。
方式2:CompletableFuture(能拿结果的异步)
如果你需要异步任务的结果(比如调第三方接口后要返回数据),用CompletableFuture
更合适。
步骤1:写一个返回 CompletableFuture 的方法
修改AsyncService
,添加一个返回CompletableFuture
的方法:
@Service
public class AsyncService {
// 异步计算一个数的平方(带返回值)
public CompletableFuture<Integer> calculateSquare(int num) {
// supplyAsync:启动一个异步任务(有返回值)
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // 模拟计算耗时1秒
return num * num;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
步骤2:调用并处理结果
在Controller里调用calculateSquare
,并通过thenApply
等方法处理结果(支持链式操作):
@RestController
public class MathController {
@Autowired
private AsyncService asyncService;
@GetMapping("/square")
public String getSquare(int num) throws Exception {
// 启动异步任务(不会阻塞)
CompletableFuture<Integer> future = asyncService.calculateSquare(num);
// 处理结果(可以继续加thenApply/thenAccept等)
future.thenApply(result -> {
System.out.println("计算结果:" + result);
return "计算完成,结果是:" + result;
});
return "正在计算..."; // 页面立刻返回
}
}
测试效果:
访问 http://localhost:8080/square?num=5
,页面立刻返回“正在计算…”,1秒后控制台打印“计算结果:25”。
五、实际业务举例(外卖下单场景)
假设你做一个外卖APP,用户下单后需要:
- 扣减库存(必须同步,否则超卖)
- 发送短信通知用户(异步,不需要等)
- 记录日志到数据库(异步,不影响主流程)
用@Async实现短信通知:
@Service
public class OrderService {
@Autowired
private AsyncService asyncService;
public String createOrder(String userId) {
// 1. 同步扣库存(必须等结果)
deductStock(userId);
// 2. 异步发通知(不等结果)
asyncService.sendSms(userId, "您的订单已提交,正在配送...");
// 3. 异步记录日志(不等结果)
asyncService.logOrder(userId);
return "订单提交成功!"; // 立刻返回用户
}
private void deductStock(String userId) {
// 模拟扣库存逻辑(耗时0.5秒)
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
}
@Service
public class AsyncService {
@Async
public void sendSms(String userId, String content) {
try {
Thread.sleep(1000); // 模拟短信接口耗时1秒
System.out.println("短信已发送给用户" + userId + ":" + content);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Async
public void logOrder(String userId) {
try {
Thread.sleep(800); // 模拟写日志耗时0.8秒
System.out.println("订单日志已记录,用户:" + userId);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
效果:用户点击“下单”后,页面0.5秒内显示“订单提交成功”,短信和日志在后台慢慢发/写,用户完全感知不到延迟。
六、注意事项(避坑)
- 异步方法不能“自调用”:
比如在OrderService
里直接写this.sendSms()
,异步会失效!必须通过@Autowired
注入自己(或其他Bean)来调用。 - 线程池要自定义:
Spring默认的异步线程池(SimpleAsyncTaskExecutor
)每次新建线程,高并发会导致“线程爆炸”。建议自己配置线程池:
@Configuration
public class AsyncConfig {
// 自定义线程池(核心10线程,最大20,队列100)
@Bean("myAsyncPool")
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("MyAsync-");
executor.initialize();
return executor;
}
}
然后在@Async
里指定线程池:
@Async("myAsyncPool") // 用自定义的线程池
public void sendSms(...) { ... }
3.异常处理要小心:
异步方法里的异常默认不会抛到主线程(会被吞掉)。解决办法:
- 用
CompletableFuture
的exceptionally
方法捕获:
future.exceptionally(e -> {
System.out.println("出错了:" + e.getMessage());
return null;
});
- 给
@Async
方法加全局异常处理器(实现AsyncUncaughtExceptionHandler
)。
七、总结
Spring Boot异步执行的核心是让耗时操作不阻塞主线程,提升系统响应速度。
- 简单场景用
@Async
(不需要结果); - 需要结果或复杂流程用
CompletableFuture
; - 一定要自定义线程池,避免资源耗尽;
- 异常处理别忘记,否则问题难找!