摘要
本文是《Spring Boot 实战派》系列的第七篇,继续深入性能优化领域。文章将聚焦于解决两类常见的业务场景:耗时操作阻塞用户请求和周期性自动化任务。
我们将首先学习如何使用 Spring 的 @Async
注解,将耗时操作(如发送邮件、生成报表)从主请求线程中剥离,实现异步执行,从而做到接口的瞬时响应,极大提升用户体验。接着,我们会深入探讨如何配置和优化异步任务的线程池。随后,文章将详细讲解如何使用 @Scheduled
注解,轻松创建强大的定时任务,并详解 cron
表达式的用法,实现如“每天凌晨执行数据清理”等自动化需求。
系列回顾:
在上一篇中,我们通过整合 Redis 缓存,极大地提升了应用“读”的性能,让高频查询接口快如闪电。但是,应用的性能瓶颈不仅仅在“读”。想象一个用户注册的场景:用户点击“注册”按钮后,系统需要创建用户、发送欢迎邮件、初始化积分… 如果这些操作都在一个请求里同步完成,用户可能要盯着加载圈转好几秒,这种糟糕的体验足以劝退大量用户。
欢迎来到性能优化的第二站!
今天,我们要解决的核心问题是:如何优雅地处理那些“慢”操作,不让它们阻塞主流程,影响用户体验。 我们将学习 Spring Boot 提供的两个强大的“多线程”利器:@Async
和 @Scheduled
。
@Async
(异步任务): 就像给耗时任务开了一个“VIP通道”。主线程把任务交给它之后,就可以立即返回,继续处理其他事情,而这个耗时任务则在后台的另一个线程里默默执行。@Scheduled
(定时任务): 就像给应用设置了一个“智能闹钟”。你可以让它在指定的时间(如每晚12点)或按固定的频率(如每5分钟)自动执行某个任务,无需人工干预。
第一部分:异步的魔力 —— 使用 @Async
提升响应速度
场景:模拟用户注册后发送欢迎邮件
我们将创建一个用户注册接口。在用户数据成功存入数据库后,需要调用一个模拟的“邮件发送服务”。这个邮件服务会故意休眠3秒,来模拟网络延迟和SMTP服务器的处理耗时。
1. 开启异步功能
与 @EnableCaching
类似,我们需要一个注解来告诉 Spring 开启异步方法执行的支持。在主启动类 MyFirstAppApplication.java
或任何一个配置类上,添加 @EnableAsync
。
@SpringBootApplication
@EnableCaching
@EnableAsync // 开启异步方法执行支持
public class MyFirstAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyFirstAppApplication.class, args);
}
}
2. 创建一个模拟的邮件服务
在 service
包下,创建一个 EmailService.java
。
package com.example.myfirstapp.service;
import com.example.myfirstapp.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
private static final Logger log = LoggerFactory.getLogger(EmailService.class);
@Async // 核心注解:将此方法标记为异步方法
public void sendWelcomeEmail(User user) {
log.info("开始向 {} 发送欢迎邮件...", user.getName());
try {
// 模拟耗时 3 秒
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("欢迎邮件已成功发送至 {} !", user.getName());
}
}
关键点:
@Async
注解被放在了sendWelcomeEmail
方法上。当其他 Bean 调用这个方法时,Spring 会拦截这个调用,将它提交到一个后台线程池中执行,然后立即返回,调用方不会被阻塞。- 重要限制: 异步方法必须是
public
的。并且,在同一个类中的方法调用(this.someAsyncMethod()
)是不会触发异步的,因为它绕过了 Spring 的代理机制。必须是通过 Spring 注入的 Bean 进行调用。
3. 在注册逻辑中调用异步方法
我们将改造 UserService
,在添加用户后,调用 EmailService
。
package com.example.myfirstapp.service;
import com.example.myfirstapp.entity.User;
// ... 其他 import
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// ...
@Autowired
private EmailService emailService;
public User registerUser(User user) {
System.out.println("主线程:开始注册用户...");
User savedUser = userRepository.save(user); // 同步保存用户
emailService.sendWelcomeEmail(savedUser); // 异步发送邮件
System.out.println("主线程:用户注册方法返回,无需等待邮件发送。");
return savedUser;
}
// ... 其他方法
}
注意:为了演示,我们创建了一个新的 registerUser
方法。
4. 测试效果
创建一个新的注册接口,并用 Postman 测试。
UserController.java
@PostMapping("/register") public Result<User> register(@RequestBody User user) { return Result.success(userService.registerUser(user)); }
测试流程:
- 启动应用。
- 用 Postman 调用
POST /users/register
,Body 中传入用户信息。 - 观察响应时间: 你会发现 Postman 几乎是瞬间就收到了响应。
- 观察控制台日志:
主线程:开始注册用户... // 日志来自 EmailService,注意线程名不是 main [ task-1] c.e.m.service.EmailService : 开始向 [用户名] 发送欢迎邮件... 主线程:用户注册方法返回,无需等待邮件发送。 // 3秒后... [ task-1] c.e.m.service.EmailService : 欢迎邮件已成功发送至 [用户名] !
日志清晰地显示,主线程在调用邮件服务后立即返回,而邮件发送的逻辑则在另一个名为 task-1
的线程中执行。我们成功地释放了主线程!
进阶:自定义异步线程池
Spring Boot 的默认异步线程池核心线程数为8,队列无限大。在生产环境中,这可能导致内存溢出。我们通常需要自定义线程池。
在 config
包下创建 AsyncConfig.java
:
package com.example.myfirstapp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(25); // 任务队列容量
executor.setThreadNamePrefix("MyAsync-"); // 线程名前缀
executor.initialize();
return executor;
}
}
在 @Async
注解中,你可以指定使用这个线程池:@Async("taskExecutor")
。
第二部分:自动化的节拍 —— 使用 @Scheduled
创建定时任务
场景:创建一个每分钟打印一次当前时间的定时任务
这在很多场景都很有用,比如:
- 每天凌晨1点,进行数据备份和清理。
- 每小时,同步一次外部数据。
- 每5分钟,检查一次系统健康状况并发送报告。
1. 开启定时任务功能
在主启动类或任何配置类上,添加 @EnableScheduling
注解。
@SpringBootApplication
@EnableCaching
@EnableAsync
@EnableScheduling // 开启定时任务支持
public class MyFirstAppApplication {
// ...
}
2. 创建定时任务类
创建一个新的 service
或 task
包,在其中创建 ScheduledTasks.java
。
package com.example.myfirstapp.task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class ScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* 使用 fixedRate,表示每隔 60 秒执行一次。
* 从上一次任务开始时计时。
*/
@Scheduled(fixedRate = 60000)
public void reportCurrentTime() {
log.info("现在时间是 (fixedRate): {}", formatter.format(LocalDateTime.now()));
}
/**
* 使用 cron 表达式,表示每分钟的第 0 秒执行(即每分钟的开始)。
* 这是最常用和最强大的方式。
*/
@Scheduled(cron = "0 * * * * ?")
public void reportCurrentTimeWithCron() {
log.info("现在时间是 (cron): {}", formatter.format(LocalDateTime.now()));
}
}
注解解读:
@Scheduled
: 核心注解,标记这是一个定时任务。fixedRate = 60000
: 表示任务执行的固定频率,单位是毫秒。无论上一次任务执行了多久,下一次任务都会在上一次任务开始后的60秒后启动。fixedDelay = 60000
: 与fixedRate
类似,但它是从上一次任务结束后开始计时。如果任务执行耗时5秒,那么下一次任务将在65秒后启动。cron = "0 * * * * ?"
: 使用 Cron 表达式,提供了极高的灵活性。
Cron 表达式详解 (从左到右):
秒 分 时 日 月 周
*
: 匹配任意值。?
: 只能用在“日”和“周”字段,表示不指定值。/
: 表示步长。0/15
在“秒”字段表示每15秒执行一次(0, 15, 30, 45)。,
: 列出枚举值。MON,WED,FRI
在“周”字段表示周一、周三、周五。-
: 表示范围。9-17
在“时”字段表示从9点到17点。
常用 Cron 表达式示例:
0 0 1 * * ?
: 每天凌晨1点执行。0 0/30 9-17 * * ?
: 每天9点到17点之间,每半小时执行一次。0 15 10 ? * MON-FRI
: 每周一至周五的上午10点15分执行。
3. 运行并观察日志
重启应用,你不需要做任何操作。静静地观察控制台日志,你会发现每隔一分钟,ScheduledTasks
里的方法就会被自动执行,并打印出当前时间。
注意: 默认情况下,所有 @Scheduled
任务共享同一个单线程。如果一个任务执行时间过长,会阻塞其他任务。如果你的定时任务很多或很耗时,建议像配置异步任务一样,配置一个专门的定时任务线程池。
总结与展望
今天,我们为应用赋予了“分身”和“自律”的能力,学会了:
- 使用
@Async
将耗时操作异步化,实现了接口的快速响应,极大地提升了用户体验。 - 如何自定义异步任务线程池,以适应生产环境的需求。
- 使用
@Scheduled
和强大的 Cron 表达式,创建了灵活可靠的定时任务,实现了应用的自动化运维。
至此,我们的应用不仅在功能、安全、配置上趋于完善,在性能表现和架构合理性上也迈上了一个新台阶。
接下来,我们将进入微服务的前哨站。一个现代化的应用,很少是孤立存在的,它需要与系统中的其他服务进行通信。在下一篇 《【微服务基石篇】服务间的对话:RestTemplate、WebClient 与 OpenFeign 对比与实战》 中,我们将学习 Spring Boot 中进行服务间 HTTP 调用的三种主流方式,为你踏入微服务世界做好最充分的准备。我们下期见!