SpringBoot整合异步任务

需求:主线程方法里面存在多个处理逻辑,其中有一个调有短信/邮箱的逻辑,如何不影响主线程堵塞,实现异步操作

1.使用线程(Thread)

创建方式

你可以直接创建并启动一个新的线程(Thread或者Runnable)
来执行阻塞操作。

1.创建一个实现Runnable接口的类,用于封装发送短信的逻辑

public class SmsSender implements Runnable {  
    private String message;  
  
    public SmsSender(String message) {  
        this.message = message;  
    }  
  
    @Override  
    public void run() {  
        // 发送短信的逻辑  
        System.out.println("Sending SMS asynchronously: " + message);  
        try {  
            // 模拟耗时操作  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        System.out.println("SMS sent asynchronously.");  
    }  
}

2.在主线程方法中创建并启动这个SmsSender线程:

public class MainThread {  
    public static void main(String[] args) {  
        // 主线程逻辑  
        System.out.println("Starting main thread logic...");  
          
        // 创建SmsSender线程,并启动它  
        String smsMessage = "Hello, this is an SMS message.";  
        Thread smsSenderThread = new Thread(new SmsSender(smsMessage));  
        smsSenderThread.start();  
          
        // 主线程继续执行其他逻辑,不会被发送短信阻塞  
        System.out.println("Continuing with main thread logic...");  
          
        // 主线程的其他逻辑...  
    }  
}

优点

  • 简单直观,易于理解。
  • 灵活性高,可以完全控制线程的生命周期。

缺点

  • 线程管理复杂,需要手动处理线程的创建、启动、同步和销毁。
  • 如果不加控制,可能导致线程过多,消耗系统资源。

是否常用

  • 较为常用,尤其在简单的并发场景下。
  • 在复杂的并发场景中,通常使用更高级的并发工具。

2.使用线程池(ThreadPool)

使用线程池(ThreadPool)来执行短信发送任务是一个高效且常用的做法。线程池能够管理一组线程,并复用这些线程来执行多个任务,从而避免频繁地创建和销毁线程,提高了应用程序的性能和响应速度。

示例代码

首先,你需要实现一个Runnable任务,用于发送短信:

public class SmsSenderTask implements Runnable {  
    private String message;  
  
    public SmsSenderTask(String message) {  
        this.message = message;  
    }  
  
    @Override  
    public void run() {  
        // 发送短信的逻辑  
        sendSms(message);  
    }  
  
    private void sendSms(String message) {  
        System.out.println("Sending SMS asynchronously: " + message);  
        // 假设发送短信需要一段时间  
        try {  
            Thread.sleep(2000); // 模拟耗时操作  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
        System.out.println("SMS sent asynchronously.");  
    }  
}

然后,在主线程中,你可以使用ExecutorService(线程池)来提交这个任务:

import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
  
public class Main {  
    public static void main(String[] args) {  
        // 创建固定大小的线程池  
        ExecutorService executor = Executors.newFixedThreadPool(5); // 假设线程池大小为5  
  
        // 主线程的其他逻辑...  
        System.out.println("Starting main thread logic...");  
  
        // 准备要发送的短信内容  
        String smsMessage = "Your verification code is 123456";  
  
        // 提交发送短信的任务到线程池  
        executor.submit(new SmsSenderTask(smsMessage));  
  
        // 主线程继续执行其他逻辑,不会被发送短信阻塞  
        System.out.println("Continuing with main thread logic...");  
        // 主线程的其他逻辑...  
  
        // 在适当的时候关闭线程池(通常在应用程序结束时)  
        // 注意:通常不建议在main方法结束时就关闭线程池,因为这可能导致还未完成的任务被中断  
        // executor.shutdown(); // 不再接受新任务,等待已提交任务完成  
        // executor.shutdownNow(); // 尝试停止所有正在执行的任务,停止处理正在等待的任务,并返回等待执行的任务列表  
  
        // 等待所有任务完成(可选,通常用于测试或确定所有任务都已完成)  
        // try {  
        //     executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);  
        // } catch (InterruptedException e) {  
        //     e.printStackTrace();  
        // }  
    }  
}

优点

  • 资源管理:线程池复用线程,避免了频繁创建和销毁线程的开销,提高了系统性能。
  • 可控性:通过调整线程池的大小,可以控制并发执行的任务数量,避免系统资源耗尽。
  • 灵活性:线程池提供了丰富的配置选项,如固定大小的线程池、缓存线程池、定时线程池等,可根据应用需求选择合适的类型。
  • 任务管理:线程池可以管理任务的提交、执行和结果获取,使得并发编程更加简单。

缺点

  • 线程泄漏:如果线程池中的任务执行时间过长或者发生异常导致线程无法回收,可能会导致线程泄漏,进而影响系统性能。
  • 资源竞争:当大量任务提交到线程池时,如果线程池中的线程数量不足以处理这些任务,可能会导致任务等待执行,从而影响系统响应速度。

是否常用

使用线程池(ExecutorService)来执行并发任务是Java中非常常用且推荐的做法。
它提供了高效的线程管理和任务执行机制,适用于各种并发场景。
在实际项目中,根据具体需求选择合适的线程池类型和配置参数是非常重要的。
Java标准库提供了多种线程池实现,如Executors.newFixedThreadPoolExecutors.newCachedThreadPool等,方便开发者快速创建和管理线程池。
此外,还可以使用第三方库(如Apache Commons Pool)来进一步扩展线程池的功能和性能。

3.使用CompletableFuture(Java 8及以上)

CompletableFuture是Java 8中新增的一个功能强大的类,它实现了FutureCompletionStage接口,提供了函数式编程的能力来处理和组合异步计算的结果。

创建方式

import java.util.concurrent.CompletableFuture;  
import java.util.concurrent.ExecutionException;  
  
public class SmsService {  
    public void sendSms(String message, String phoneNumber) {  
        // 假设这是发送短信的实际逻辑  
        System.out.println("Sending SMS to " + phoneNumber + " with message: " + message);  
        // 模拟耗时操作  
        try {  
            Thread.sleep(2000); // 假设发送短信需要2秒  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
    }  
}  
  
public class Main {  
    public static void main(String[] args) throws ExecutionException, InterruptedException {  
        SmsService smsService = new SmsService();  
        // 主线程逻辑  
        System.out.println("Starting main thread logic..."); 
  
        // 使用CompletableFuture异步发送短信  
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {  
            smsService.sendSms("Hello", "1234567890");  
        });  
  
        // 主线程继续执行其他逻辑,不会被阻塞  
        System.out.println("Main thread is doing other tasks...");  
  
        // 如果需要等待异步任务完成,可以调用future.get(),但这会阻塞直到任务完成  
        // 通常不推荐在主线程中直接调用get(),除非你确定这样做不会造成问题  
        // future.get(); // 这会阻塞,直到sendSms方法执行完毕  
    }  
}

优点

  1. 非阻塞CompletableFuture允许你在主线程中启动一个异步任务,而不需要等待该任务完成。
  2. 链式调用:支持链式调用,可以方便地对异步结果进行进一步处理。
  3. 组合异步操作:可以轻松组合多个异步操作,比如将多个CompletableFuture的结果合并成一个。
  4. 异常处理:提供了对异步操作中可能发生的异常的处理能力。

缺点

  1. 学习曲线:相对于传统的线程处理方式,CompletableFuture的使用可能有一定的学习成本。
  2. 错误处理:如果没有正确使用异常处理,可能会导致未捕获的异常,进而引发程序问题。

是否常用

CompletableFuture在Java 8及以上版本的应用中非常常用,特别是在需要处理异步操作、组合多个异步结果或提高程序响应性的场景下。它提供了一种简洁而强大的方式来处理并发编程中的复杂问题,是现代Java并发编程中的重要工具之一。然而,对于简单的同步任务或者不需要复杂并发控制的场景,可能不需要使用CompletableFuture

4.Spring的@Async异步 (目前这个最好用)

方法一:直接使用 (普通的就直接使用即可)

  1. 添加 @EnableAsync 注解。在主类上(启动类)或者 某个类上,一般是放在启动类上
    否则,异步方法不会生效
  2. 把发短信的逻辑抽出来,单独一个方法,在其方法上使用异步注解@Async,请确保该方法所处的类是一个Spring管理的bean(例如,通过@Component@Service等注解标记)。
  3. 被@Async标记的方法不能被 static 修饰

@Async注解失效情况:

  1. 在@SpringBootApplication启动类没有添加注解@EnableAsync
  2. 调用方法和异步方法写在同一个类,需要在不同的类才能有效。
    也就是抽出去的 “需异步的业务逻辑(例如:发短信)” 的方法 应该放到另一个类去
  3. 调用的是静态(static)方法
  4. 调用(private)私有化方法

个别失效报错情况:
@EnableAsync上设置proxyTargetClass=true来强制使用基于cglib的代理即可
如下操作

@EnableAsync(proxyTargetClass = true)

创建方式
1.SpringApplication启用注解@EnableAsync


@SpringBootApplication
@EnableAsync
public class Application {
 
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Controller层

@RestController
@RequestMapping({"/policy"})
@Api(value = "xx-api", tags = "相关接口")
public class SmsController {
@RequestMapping("/xxx/xxx/cancelAudit")
    @ResponseBody
    public String cancelAudit(DefectForm defectForm){
        defectRecordService.cancelAudit(defectForm); //更新操作,成功后往下走,sendCancelAuditMsg会新启一个线程处理,主线程继续往下走,走到return "ok";返回
        //审核拒绝:业务操作完成后发短信
        defectRecordService.sendCancelAuditMsg(defectForm);
        return "ok";
    }
}

SmsServiceImpl 层

@Service
public class SmsServiceImpl implements SmsService {
//这里我们就不需要添加异步注解了
public void cancelAudit(DefectForm defectForm) {
        Map<String,Object> params = new HashMap<>();
        params.put("defectId", defectForm.getDefectId()); //缺陷记录ID
        params.put("defectStatus", 3); //更新缺陷记录状态审核拒绝
        params.put("reason", defectForm.getReason()); //拒绝理由
        defectRecordDao.updateByPrimaryKeySelective(params);
    }
}

sendCancelAuditMsg(抽出去的方法)和cancelAudit(调用方法)不能在同一个类里面

//把发短信的逻辑抽出来,单独一个方法,使用异步注解
@Async
public void sendCancelAuditMsg(DefectForm defectForm){
    //审核拒绝发送短信,短信发送给缺陷上报人,缺陷内容,审核拒绝理由
    Account account = accountDao.findAccountById(defectForm.getCreatorUserid());
    if(account != null && StringUtils.isNotBlank(account.getMobile())){
        String mobile = account.getMobile();
        String defectContent = defectForm.getDefectContent();
        String reason = defectForm.getReason();
        Map<String,String> templateData = new HashMap<>();
        templateData.put("defectContent", defectContent);
        templateData.put("reason", reason);
        smsService.sendSms(null, mobile, SmsConstant.DEFECT_REFUSRD_CODE, templateData,false);
        logger.debug("缺陷上报记录审核拒绝,发送短信给缺陷记录上报人******");
    }
}

方法二:自定义线程池(看业务性的要求)

/**
 * 线程池参数配置,多个线程池实现线程池隔离,
 * @Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,
 * 在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName")
 **/
import org.springframework.context.annotation.Configuration;  
import org.springframework.scheduling.annotation.EnableAsync;  
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;  
import org.springframework.context.annotation.Bean;  
  
@Configuration  
@EnableAsync  
public class AsyncConfig {  
  
    @Bean(name = "taskExecutor")  
    public Executor taskExecutor() {  
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
        //设置线程池的核心线程数
        executor.setCorePoolSize(5);  
        //设置线程池的最大线程数
        executor.setMaxPoolSize(10);  
        //线程池的工作队列容量
        executor.setQueueCapacity(25);  
        //线程池中线程的名称前缀
        executor.setThreadNamePrefix("Async-");  
        //设置自定义的拒绝策略
        executor.setRejectedExecutionHandler((r, e) -> {  
    try {  
        // 记录一个警告日志,说明当前保存评价的连接池已满,触发了拒绝策略。  
        log.warn("保存评价连接池任务已满,触发拒绝策略");  
          
        // 尝试将任务重新放入队列中,等待30秒。  
        // 如果在这30秒内队列有空闲空间,任务将被成功放入队列;否则,offer方法将返回false。  
        boolean offer = e.getQueue().offer(r, 30, TimeUnit.SECONDS);  
          
        // 记录日志,显示等待30秒后尝试重新放入队列的结果。  
        log.warn("保存评价连接池任务已满,拒绝接收任务,等待30s重新放入队列结果rs:{}", offer);  
    } catch (InterruptedException ex) {  
        // 如果在等待过程中线程被中断,捕获InterruptedException异常。  
        // 记录一个错误日志,说明在尝试重新放入队列时发生了异常。  
        log.error("【保存评价】连接池任务已满,拒绝接收任务了,再重新放入队列后出现异常", ex);  
          
        // 构建一条警告消息,其中包含线程池的各种信息(如池大小、活动线程数、核心线程数等)。  
        String msg = String.format("保存评价线程池拒绝接收任务! Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)"  
                , e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(),  
                e.getMaximumPoolSize(), e.getLargestPoolSize(), e.getTaskCount(),  
                e.getCompletedTaskCount());  
          
        // 记录包含线程池详细信息的警告日志。  
        log.warn(msg);  
    }  
});
        //初始化线程池,线程池就会处于可以接收任务的状态
        executor.initialize();  
        return executor;  
    }  
}

接下来,在需要异步执行的方法上使用@Async注解。
例如,在SmsService服务类中:

import org.springframework.scheduling.annotation.Async;  
import org.springframework.stereotype.Service;  
  
@Service  
public class SmsService {  
  
    // 使用@Async注解来标识该方法为异步方法  
    @Async("taskExecutor") // 可以指定使用哪个TaskExecutor,这里使用上面定义的taskExecutor  
    public void sendSms(String message, String phoneNumber) {  
        // 调用短信接口发送短信  
        System.out.println("Sending SMS to " + phoneNumber + " with message: " + message);  
  
        // 模拟耗时操作  
        try {  
            Thread.sleep(2000); // 假设发送短信需要2秒  
        } catch (InterruptedException e) {  
            Thread.currentThread().interrupt();  
        }  
    }  
}

在主线程中调用异步方法时,无需做任何特殊处理,Spring会自动在后台线程中执行它:

import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.stereotype.Component;  
  
@Component  
public class MainComponent {  
  
    private final SmsService smsService;  
  
    @Autowired  
    public MainComponent(SmsService smsService) {  
        this.smsService = smsService;  
    }  
  
    public void doWork() {  
        // 主线程逻辑  
        System.out.println("Starting main thread logic...");
         
        // 调用异步方法发送短信,不会阻塞主线程  
        smsService.sendSms("Hello", "1234567890");  
  
        // 主线程继续执行其他逻辑  
        System.out.println("Main thread is doing other tasks...");  
    }  
}

优点

  1. 非阻塞调用:异步方法调用不会阻塞主线程,使得主线程可以继续执行其他任务。
  2. 易于集成:在Spring项目中,通过简单的注解和配置即可集成异步执行功能。
  3. 线程池管理:通过自定义TaskExecutor,可以方便地管理异步任务的线程池,包括设置核心线程数、最大线程数等。

缺点

  1. 异常处理:异步方法中的异常不会自动传播到调用线程,需要特别注意异常处理。
  2. 事务边界:异步方法中的事务边界可能与调用线程的事务边界不一致,需要仔细处理事务逻辑。
  3. 调试困难:由于异步方法在不同的线程中执行,调试时可能会更加困难,需要跟踪异步任务的执行情况。

创建方式是否常用

是的@Async注解在Spring项目中非常常用,
尤其是当需要执行耗时的操作而又不希望阻塞主线程时。
它提供了一种简单而强大的方式来处理并发和异步任务。
然而,对于非Spring项目或者需要更精细控制并发行为的场景,可能需要使用其他并发编程工具或框架。

记得在调用异步方法的类上加上@EnableAsync注解,
并确保该类是一个Spring管理的bean(例如,通过@Component@Service等注解标记)。
这样,Spring才能识别并代理@Async注解的方法,使其异步执行。

总结

文章参考
【1】这8种java异步实现方式,性能炸裂!
https://blog.csdn.net/afreon/article/details/128825831
【2】【SpringBoot】SpringBoot 中使用 @Async 实现优雅地异步调用
https://blog.csdn.net/sco5282/article/details/122873631
【3】Spring Boot 微服务异步调用 @EnableAsync @Async
https://www.jianshu.com/p/b4fcac54bed6
【4】SpringBoot @Async 异步处理业务逻辑和发短信逻辑
https://www.cnblogs.com/ph7seven/p/9549824.html
【5】SpringBoot项目整合阿里云短信业务(非常详细)
https://blog.csdn.net/qq_40939438/article/details/134089344
【6】SpringBoot整合异步任务实现发送邮件
https://blog.csdn.net/weixin_45821811/article/details/119685108

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值