cms项目细节-性能优化之异步化
很多接口真正的业务很短,但还要等日志落库、发通知、统计 PV。如果这些都是同步执行,那么只要一个旁路慢,整个接口就被拖慢。用户点一下“提交留言”,在后台却要等“发邮件”完成才返回。
public void submitMessage(...) {
saveMessage(...); // 主业务
saveLog(...); // 慢的旁路:等它写完才返回
sendNotify(...); // 更慢的旁路:调外部接口
return;
}异步化:把某些工作从当前请求线程挪到另一个线程执行,让主线程立刻返回
解决方案:@Async + 线程池
@Async 和 @Transactional、@RequireLevel 一样,本质都是 Spring AOP 动态代理。你写的:
@Service
public class NotifyService {
@Async("mainAsyncExecutor")
public void sendNotify(String content) {
// 真正逻辑
}
}Spring 启动时会给 NotifyService 生成一个代理对象。外部调用 sendNotify() 时,实际走的是代理:
调用方 → NotifyService 代理
→ 把方法提交到线程池(立刻返回)
→ 线程池里的工作线程真正执行 sendNotify 方法体所以主线程只是“提交任务”就返回了,真正的逻辑放到了线程池里的另一个线程。
前提:开启@EnableAsync
@Configuration
@EnableAsync
public class AsyncConfig {
}没有它,@Async 注解会被忽略,方法还是在主线程同步执行——这是最常见的“@Async 不生效”原因之一。
不要使用默认线程池
如果没有自定义线程池,Spring 默认用 SimpleAsyncTaskExecutor。它的问题:每次执行都新建一个线程,用完不回收。高并发下会无限创建线程,直接 OOM 或把系统拖垮。
所以生产必须自定义 ThreadPoolTaskExecutor:
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean("mainAsyncExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(4); // 核心线程数
pool.setMaxPoolSize(8); // 最大线程数
pool.setQueueCapacity(100); // 队列容量
pool.setKeepAliveSeconds(60); // 非核心线程空闲存活秒数
pool.setThreadNamePrefix("async-"); // 线程名前缀,方便排查
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
pool.initialize();
return pool;
}
}| 参数 | 含义 | 常见取值 |
|---|---|---|
corePoolSize | 常驻线程数,即使空闲也保留 | CPU 密集型≈CPU 核数;IO 密集型可稍大 |
maxPoolSize | 线程数上限 | 核心×2 或按需 |
queueCapacity | 排队任务上限 | 视吞吐量,别设无限 |
keepAliveSeconds | 超过核心数的线程,空闲多久后回收 | 60 |
threadNamePrefix | 线程名前缀 | async-,方便看日志 |
rejectedExecutionHandler | 队列满且线程到上限时怎么办 | 默认 AbortPolicy 抛异常 |
新任务来了:
- 核心线程还没满 → 直接开一个核心线程执行
- 核心线程满了 → 放进队列
- 队列满了 → 再开新的(非核心)线程,但不能超过 maxPoolSize
- maxPoolSize 也满了、队列也满了 → 触发拒绝策略
拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) | 直接抛 RejectedExecutionException |
CallerRunsPolicy | 让提交任务的线程自己执行(相当于退回主线程序列化) |
DiscardOldestPolicy | 丢弃队列里最老的任务,再提交新任务 |
DiscardPolicy | 静默丢弃新任务 |
返回类型:void 还是 Future
- 方法返回
void:主线程提交后立刻返回,不关心结果。适合“通知、日志”。 - 方法返回
Future/CompletableFuture:可以拿到结果。适合“需要结果但不想阻塞”的场景。
@Async("mainAsyncExecutor")
public CompletableFuture<String> generateReport() {
return CompletableFuture.completedFuture("...");
}最小演示
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("mailExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(2);
pool.setMaxPoolSize(4);
pool.setQueueCapacity(50);
pool.setThreadNamePrefix("mail-");
pool.initialize();
return pool;
}
}
@Service
public class MailService {
@Async("mailExecutor")
public void send(String to) {
// 被丢到线程池执行
}
}
@RestController
class UserController {
@Autowired MailService mailService;
@PostMapping("/register")
public String register() {
mailService.send("a@b.com"); // 立即返回ok
return "ok";
}
}| 位置 | 现在做的事 | 为什么适合异步 |
|---|---|---|
service/AuthService.saveLog() | 登录/改密/切换区域后同步写 ay_syslog | 日志是旁路,用户不关心,不该拖慢登录 |
service/PublicInteractionService.addMessage() | 提交留言后同步写 ay_message | 保存完就返回,通知/统计可异步 |
service/PublicInteractionService.addComment() | 提交评论后同步写 ay_member_comment | 同上 |
service/DynamicFormService.submit() | 表单提交同步插入 | 保存完可异步发通知/埋点 |
service/PublicApiService.likes()/oppose() | 点赞/反对同步 update 计数 | 计数可异步,接口更快 |
service/DatabaseService.exportSql() | 大表导出 SQL | 很重,可异步返回任务号(进阶) |
收益:
- 登录、留言、点赞等接口响应更快(真正等的东西变少了)。
- 主线程负载降低,能处理更多请求。
- 日志/通知这类偶发失败不影响主业务。
代价(一定要主动说):
- 丢失内存:异常在子线程里,如果不处理会静默丢失。
- 上下文丢失:子线程拿不到
SecurityContextHolder/RequestContextHolder(需手动传递)。 - 事务隔离:异步方法最好不要依赖外层事务(它自己一个新的线程/事务上下文)。
- 顺序/乱序:异步任务执行顺序不保证。
- 幂等:通知/计数要防重复。
| 问题 | 答案 |
|---|---|
| @Async 原理 | 基于 AOP 代理,把方法提交到线程池,主线程立即返回 |
| 为什么必须自定义线程池 | 默认 SimpleAsyncTaskExecutor 每次 new 线程,不可控 |
| 线程池执行顺序 | 核心线程 → 队列 → 非核心线程(≤max)→ 拒绝策略 |
| 返回类型限制 | void 或 Future/CompletableFuture |
| 自调用会生效吗 | 不会,同 AOP 自调用问题 |
| 异常怎么处理 | 方法内 try/catch,或 AsyncUncaughtExceptionHandler |
| 子线程有 SecurityContext 吗 | 没有,需要主线程取出后当参数传入 |
| 能拿到外层事务吗 | 不能,异步方法独立线程,可自行加 @Transactional |
| 更适合 @Async 还是 MQ | 轻量旁路用 @Async;需要削峰/解耦/可靠投递用 MQ |
6. 常见坑:为什么 @Async 不生效
坑 1:没写 @EnableAsync
最容易被忽略。没有它,@Async 被当成普通注解忽略,方法照常在主线程同步跑。
坑 2:自调用 和 AOP 一样:
@Service
public class NotifyService {
public void a() {
this.sendNotify("x"); // this 是真实对象,不走代理 → @Async 不生效
}
@Async
public void sendNotify(String s) { ... }
}解决:sendNotify 放到另一个 bean,由外部调用。
坑 3:private 方法
代理无法拦截 private 方法,@Async 必须放在可被代理调用的 public 方法上。
坑 4:返回类型不是 void/Future
如果方法是同步返回普通对象(如 String),Spring 会报错或不做异步。要么 void,要么 Future/CompletableFuture。
坑 5:异常没有处理
子线程抛异常,主线程感知不到。要么在方法内 try/catch,要么配置 AsyncUncaughtExceptionHandler 统一处理:
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) ->
log.error("Async method {} failed", method.getName(), throwable);
}
}坑 6:上下文丢失
子线程里 SecurityContextHolder.getContext()、RequestContextHolder.getRequestAttributes() 都是空的,因为线程池线程不继承发起线程的上下文。需要在调用前取出,作为参数传入异步方法:
String username = SecurityUtils.currentUser().getUsername(); // 主线程先取
asyncService.saveLog(..., username); // 再传进去坑 7:事务边界
@Async 方法自己在一个新线程执行,加在外层方法上的 @Transactional 不会覆盖到它。如果异步方法也要事务,就在异步方法自身加 @Transactional。
坑 8:线程池参数不合理 队列无限增长会 OOM;任务堆积说明消费跟不上,需要调大线程数或用 MQ 削峰。监控线程池指标(活跃线程数、队列长度)。
坑 9:拒绝策略没想清楚
任务爆了默认抛异常,如果没捕获就丢任务。生产常用 CallerRunsPolicy 做兜底,或把任务丢进 MQ。