网站建设目录,网站推广的一般流程是,宿州公司做网站,免费虚拟主机免备案文章目录 前言什么时候需要异步调用常见同步耗时问题系统资源被白白浪费 异步调用实现方式Async注解消息队列 线程池配置要合理Async注解失效问题总结 前言
说起异步调用#xff0c;我想起刚学习微服务那会儿做的一个电商项目。用户下单后#xff0c;系统要扣库存、发短信、… 文章目录 前言什么时候需要异步调用常见同步耗时问题系统资源被白白浪费 异步调用实现方式Async注解消息队列 线程池配置要合理Async注解失效问题总结 前言
说起异步调用我想起刚学习微服务那会儿做的一个电商项目。用户下单后系统要扣库存、发短信、记日志…一大堆操作。当时图省事全部写在一个接口里同步执行。结果用户点完下单按钮页面要转10多秒才有反应用户还以为系统卡死了经常重复点击。后来排查发现发短信的第三方接口响应特别慢所有订单请求都堵在那里等着。那一刻才真正意识到异步调用的重要性。 现在回头看异步调用不仅仅是技术优化更是用户体验的保证。今天就来聊聊Spring Boot中异步调用的各种实现方式从最简单的Async到消息队列。
什么时候需要异步调用
常见同步耗时问题
想象一下这些场景用户上传头像页面转圈转了30秒用户注册成功等了半天才跳转还不知道是不是真的成功了导出个Excel报表浏览器直接超时…其实这些问题的根源都一样把不需要立即返回结果的操作和核心业务流程耦合在一起了。用户注册核心是把用户信息存到数据库至于发欢迎邮件、初始化用户数据这些完全可以后台慢慢处理。
系统资源被白白浪费
还有一个更隐蔽的问题资源浪费。Java的线程模型是一个请求对应一个线程如果线程都在等I/O操作数据库查询、文件读写CPU其实是闲着的但线程资源却被占用了。
异步调用实现方式
Async注解
对于刚接触异步编程的同学Async绝对是最友好的选择。只需要加个注解并自定义线程池Spring就帮你把方法丢到线程池里执行。
Configuration
EnableAsync
public class AsyncConfig {/**配置了一个名为taskExecutor的线程池核心线程数10始终保持活跃的线程数量最大线程数20当队列满时可以扩展到的最大线程数队列容量200任务队列能容纳的最大任务数线程名前缀Task-便于日志追踪*/Bean(taskExecutor)public TaskExecutor taskExecutor() {ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor();executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(200); executor.setThreadNamePrefix(Task-);executor.initialize();return executor;}
}Service
public class UserService {Async(taskExecutor)public void sendWelcomeEmail(String email) {try {// 模拟发邮件耗时Thread.sleep(2000);log.info(欢迎邮件发送成功: {}, email);} catch (Exception e) {log.error(邮件发送失败, e);}}Transactionalpublic User registerUser(UserRegisterDTO dto) {// 核心注册逻辑快速返回User user new User();user.setEmail(dto.getEmail());user.setUsername(dto.getUsername());User savedUser userRepository.save(user);// 异步发送欢迎邮件sendWelcomeEmail(user.getEmail());return savedUser;}
}在代码中registerUser() 会在调用 sendWelcomeEmail() 后立刻继续执行并返回结果邮件发送由后台线程处理。这种方式的好处是简单直接。但有个坑要注意Async注解在同一个类的方法调用中是不生效的因为Spring AOP的限制。如果需要在同一个类里调用异步方法要么注入自己要么把异步方法提取到单独的Service里。
消息队列
当系统拆分成多个微服务后异步处理就不仅仅是单个应用内部的事了。这时候消息队列就派上用场了。我个人比较喜欢RabbitMQ配置简单功能够用。
Configuration
EnableRabbit
public class RabbitConfig {Beanpublic DirectExchange userExchange() {return new DirectExchange(user.exchange);}Beanpublic Queue userRegisteredQueue() {return QueueBuilder.durable(user.registered.queue).build();}Beanpublic Binding userRegisteredBinding() {return BindingBuilder.bind(userRegisteredQueue()).to(userExchange()).with(user.registered);}
}// 用户服务 - 消息发送方
Service
public class UserService {Autowiredprivate RabbitTemplate rabbitTemplate;Transactionalpublic User registerUser(UserRegisterDTO dto) {User user new User();user.setEmail(dto.getEmail());user.setUsername(dto.getUsername());User savedUser userRepository.save(user);// 发送用户注册消息UserRegisteredMessage message new UserRegisteredMessage(savedUser.getId(), savedUser.getEmail(), savedUser.getUsername());rabbitTemplate.convertAndSend(user.exchange, user.registered, message);log.info(用户注册消息已发送: {}, savedUser.getId());return savedUser;}
}// 通知服务 - 消息接收方
Component
public class NotificationService {RabbitListener(queues user.registered.queue)public void handleUserRegistered(UserRegisteredMessage message) {try {log.info(收到用户注册消息: {}, message.getUserId());// 发送欢迎邮件emailService.sendWelcomeEmail(message.getEmail(), message.getUsername());// 发送欢迎短信smsService.sendWelcomeSms(message.getPhone());log.info(用户注册后续处理完成: {}, message.getUserId());} catch (Exception e) {log.error(处理用户注册消息失败: {}, message.getUserId(), e);// 这里可以实现重试逻辑或者发送到死信队列throw e;}}
}消息队列的好处是彻底解耦用户服务不需要知道有哪些下游服务只管发消息就行。而且天然支持重试、死信队列等容错机制。
不过消息队列也带来了复杂性消息顺序、重复消费、消息丢失等问题都需要考虑。我的建议是如果是单体应用优先考虑使用Async注解如果已经是微服务架构那消息队列是必选项。
线程池配置要合理
异步调用的核心是线程池配置不当会适得其反。我见过有的项目线程池设置得太小异步任务排队等待还不如同步执行快也见过设置得太大结果把系统内存撑爆了。
Configuration
public class AsyncExecutorConfig {// CPU密集型任务Bean(cpuTaskExecutor)public TaskExecutor cpuTaskExecutor() {ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor();// CPU密集型核心线程数 CPU核数int processors Runtime.getRuntime().availableProcessors();executor.setCorePoolSize(processors);executor.setMaxPoolSize(processors * 2);executor.setQueueCapacity(50);executor.setThreadNamePrefix(cpu-task-);return executor;}// I/O密集型任务Bean(ioTaskExecutor)public TaskExecutor ioTaskExecutor() {ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor();// I/O密集型可以设置更多线程executor.setCorePoolSize(20);executor.setMaxPoolSize(50);executor.setQueueCapacity(200);executor.setThreadNamePrefix(io-task-);executor.setKeepAliveSeconds(60);return executor;}
}一般来说CPU密集型任务的线程数设置为CPU核数比较合适I/O密集型任务可以设置更多线程具体数量需要根据实际情况调优。
Async注解失效问题
这个坑我估计每个Spring开发者都踩过。Async在同一个类的方法调用中是不生效的因为Spring AOP的代理机制限制。 错误写法
Service
public class UserService {public void registerUser(User user) {userRepository.save(user);sendWelcomeEmail(user.getEmail()); // 这里调用不会异步执行}Asyncpublic void sendWelcomeEmail(String email) {// 发送邮件逻辑}
}正确写法 // 正确写法1提取到单独的Service
Service
public class UserService {Autowiredprivate NotificationService notificationService;public void registerUser(User user) {userRepository.save(user);notificationService.sendWelcomeEmail(user.getEmail());}
}Service
public class NotificationService {Asyncpublic void sendWelcomeEmail(String email) {// 发送邮件逻辑}
}总结
异步调用是现代Java应用开发的必备技能选择合适的实现方式很重要。简单的异步操作用Async就够了配置简单上手快。如果是微服务架构消息队列是必选项解耦彻底扩展性好。