天天看點

如何在SpringBoot中異步請求和異步調用一、SpringBoot 中異步請求的使用二、SpringBoot 中異步調用的使用三、異步請求與異步調用的差別

原文:cnblogs.com/baixianlong/p/10661591.html

一、SpringBoot 中異步請求的使用

1、異步請求與同步請求

如何在SpringBoot中異步請求和異步調用一、SpringBoot 中異步請求的使用二、SpringBoot 中異步調用的使用三、異步請求與異步調用的差別

特點:

可以先釋放容器配置設定給請求的線程與相關資源,減輕系統負擔,釋放了容器所配置設定線程的請求,其響應将被延後,可以在耗時處理完成(例如長時間的運算)時再對用戶端進行響應。

一句話:增加了伺服器對用戶端請求的吞吐量(實際生産上我們用的比較少,如果并發請求量很大的情況下,我們會通過 nginx 把請求負載到叢集服務的各個節點上來分攤請求壓力,當然還可以通過消息隊列來做請求的緩沖)。

2、異步請求的實作

方式一:Servlet 方式實作異步請求

@RequestMapping(value = "/email/servletReq", method = GET)
  public void servletReq (HttpServletRequest request, HttpServletResponse response) {
      AsyncContext asyncContext = request.startAsync();
      //設定監聽器:可設定其開始、完成、異常、逾時等事件的回調處理
      asyncContext.addListener(new AsyncListener() {
          @Override
          public void onTimeout(AsyncEvent event) throws IOException {
              System.out.println("逾時了...");
              //做一些逾時後的相關操作...
          }
          @Override
          public void onStartAsync(AsyncEvent event) throws IOException {
              System.out.println("線程開始");
          }
          @Override
          public void onError(AsyncEvent event) throws IOException {
              System.out.println("發生錯誤:"+event.getThrowable());
          }
          @Override
          public void onComplete(AsyncEvent event) throws IOException {
              System.out.println("執行完成");
              //這裡可以做一些清理資源的操作...
          }
      });
      //設定逾時時間
      asyncContext.setTimeout(20000);
      asyncContext.start(new Runnable() {
          @Override
          public void run() {
              try {
                  Thread.sleep(10000);
                  System.out.println("内部線程:" + Thread.currentThread().getName());
                  asyncContext.getResponse().setCharacterEncoding("utf-8");
                  asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
                  asyncContext.getResponse().getWriter().println("這是異步的請求傳回");
              } catch (Exception e) {
                  System.out.println("異常:"+e);
              }
              //異步請求完成通知
              //此時整個請求才完成
              asyncContext.complete();
          }
      });
      //此時之類 request的線程連接配接已經釋放了
      System.out.println("主線程:" + Thread.currentThread().getName());
  }

           

複制

方式二:使用很簡單,直接傳回的參數包裹一層 callable 即可,可以繼承 WebMvcConfigurerAdapter 類來設定預設線程池和逾時處理

@RequestMapping(value = "/email/callableReq", method = GET)
  @ResponseBody
  public Callable<String> callableReq () {
      System.out.println("外部線程:" + Thread.currentThread().getName());
      return new Callable<String>() {
          @Override
          public String call() throws Exception {
              Thread.sleep(10000);
              System.out.println("内部線程:" + Thread.currentThread().getName());
              return "callable!";
          }
      };
  }
  @Configuration
  public class RequestAsyncPoolConfig extends WebMvcConfigurerAdapter {
  @Resource
  private ThreadPoolTaskExecutor myThreadPoolTaskExecutor;
  @Override
  public void configureAsyncSupport(final AsyncSupportConfigurer configurer) {
      //處理 callable逾時
      configurer.setDefaultTimeout(60*1000);
      configurer.setTaskExecutor(myThreadPoolTaskExecutor);
      configurer.registerCallableInterceptors(timeoutCallableProcessingInterceptor());
  }
  @Bean
  public TimeoutCallableProcessingInterceptor timeoutCallableProcessingInterceptor() {
      return new TimeoutCallableProcessingInterceptor();
  }
}

           

複制

方式三:和方式二差不多,在 Callable 外包一層,給 WebAsyncTask 設定一個逾時回調,即可實作逾時處理

@RequestMapping(value = "/email/webAsyncReq", method = GET)
    @ResponseBody
    public WebAsyncTask<String> webAsyncReq () {
        System.out.println("外部線程:" + Thread.currentThread().getName());
        Callable<String> result = () -> {
            System.out.println("内部線程開始:" + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (Exception e) {
                // TODO: handle exception
            }
            logger.info("副線程傳回");
            System.out.println("内部線程傳回:" + Thread.currentThread().getName());
            return "success";
        };
        WebAsyncTask<String> wat = new WebAsyncTask<String>(3000L, result);
        wat.onTimeout(new Callable<String>() {
            @Override
            public String call() throws Exception {
                // TODO Auto-generated method stub
                return "逾時";
            }
        });
        return wat;
    }

           

複制

方式四:DeferredResult 可以處理一些相對複雜一些的業務邏輯,最主要還是可以在另一個線程裡面進行業務處理及傳回,即可在兩個完全不相幹的線程間的通信。

@RequestMapping(value = "/email/deferredResultReq", method = GET)
    @ResponseBody
    public DeferredResult<String> deferredResultReq () {
        System.out.println("外部線程:" + Thread.currentThread().getName());
        //設定逾時時間
        DeferredResult<String> result = new DeferredResult<String>(60*1000L);
        //處理逾時事件 采用委托機制
        result.onTimeout(new Runnable() {
            @Override
            public void run() {
                System.out.println("DeferredResult逾時");
                result.setResult("逾時了!");
            }
        });
        result.onCompletion(new Runnable() {
            @Override
            public void run() {
                //完成後
                System.out.println("調用完成");
            }
        });
        myThreadPoolTaskExecutor.execute(new Runnable() {
            @Override
            public void run() {
                //處理業務邏輯
                System.out.println("内部線程:" + Thread.currentThread().getName());
                //傳回結果
                result.setResult("DeferredResult!!");
            }
        });
       return result;
    }

           

複制

二、SpringBoot 中異步調用的使用

1、介紹

異步請求的處理。除了異步請求,一般上我們用的比較多的應該是異步調用。通常在開發過程中,會遇到一個方法是和實際業務無關的,沒有緊密性的。比如記錄日志資訊等業務。這個時候正常就是啟一個新線程去做一些業務處理,讓主線程異步的執行其他業務。

2、使用方式(基于 spring 下)

需要在啟動類加入 @EnableAsync 使異步調用 @Async 注解生效

在需要異步執行的方法上加入此注解即可 @Async("threadPool"),threadPool 為自定義線程池

代碼略。。。就倆标簽,自己試一把就可以了

3、注意事項

在預設情況下,未設定 TaskExecutor 時,預設是使用 SimpleAsyncTaskExecutor 這個線程池,但此線程不是真正意義上的線程池,因為線程不重用,每次調用都會建立一個新的線程。可通過控制台日志輸出可以看出,每次輸出線程名都是遞增的。是以最好我們來自定義一個線程池。

調用的異步方法,不能為同一個類的方法(包括同一個類的内部類),簡單來說,因為 Spring 在啟動掃描時會為其建立一個代理類,而同類調用時,還是調用本身的代理類的,是以和平常調用是一樣的。

其他的注解如 @Cache 等也是一樣的道理,說白了,就是 Spring 的代理機制造成的。是以在開發中,最好把異步服務單獨抽出一個類來管理。下面會重點講述。

4、什麼情況下會導緻 @Async 異步方法會失效?

  • a. 調用同一個類下注有 @Async 異步方法:在 spring 中像 @Async 和 @Transactional、cache 等注解本質使用的是動态代理,其實 Spring 容器在初始化的時候 Spring 容器會将含有 AOP 注解的類對象 “替換” 為代理對象(簡單這麼了解),那麼注解失效的原因就很明顯了,就是因為調用方法的是對象本身而不是代理對象,因為沒有經過 Spring 容器,那麼解決方法也會沿着這個思路來解決。
  • b. 調用的是靜态 (static) 方法
  • c. 調用 (private) 私有化方法

5、解決 4 中問題 1 的方式(其它 2,3 兩個問題自己注意下就可以了)

将要異步執行的方法單獨抽取成一個類,原理就是當你把執行異步的方法單獨抽取成一個類的時候,這個類肯定是被 Spring 管理的,其他 Spring 元件需要調用的時候肯定會注入進去,這時候實際上注入進去的就是代理類了。

其實我們的注入對象都是從 Spring 容器中給目前 Spring 元件進行成員變量的指派,由于某些類使用了 AOP 注解,那麼實際上在 Spring 容器中實際存在的是它的代理對象。那麼我們就可以通過上下文擷取自己的代理對象調用異步方法。

@Controller
@RequestMapping("/app")
public class EmailController {
    //擷取ApplicationContext對象方式有多種,這種最簡單,其它的大家自行了解一下
    @Autowired
    private ApplicationContext applicationContext;
    @RequestMapping(value = "/email/asyncCall", method = GET)
    @ResponseBody
    public Map<String, Object> asyncCall () {
        Map<String, Object> resMap = new HashMap<String, Object>();
        try{
            //這樣調用同類下的異步方法是不起作用的
            //this.testAsyncTask();
            //通過上下文擷取自己的代理對象調用異步方法
            EmailController emailController = (EmailController)applicationContext.getBean(EmailController.class);
            emailController.testAsyncTask();
            resMap.put("code",200);
        }catch (Exception e) {
            resMap.put("code",400);
            logger.error("error!",e);
        }
        return resMap;
    }
    //注意一定是public,且是非static方法
    @Async
    public void testAsyncTask() throws InterruptedException {
        Thread.sleep(10000);
        System.out.println("異步任務執行完成!");
    }
}

           

複制

6、開啟 cglib 代理,手動擷取 Spring 代理類, 進而調用同類下的異步方法。

首先,在啟動類上加上 @EnableAspectJAutoProxy(exposeProxy = true) 注解。

代碼實作,如下:

@Service
@Transactional(value = "transactionManager", readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = Throwable.class)
public class EmailService {
    @Autowired
    private ApplicationContext applicationContext;
    @Async
    public void testSyncTask() throws InterruptedException {
        Thread.sleep(10000);
        System.out.println("異步任務執行完成!");
    }
    public void asyncCallTwo() throws InterruptedException {
        //this.testSyncTask();
//        EmailService emailService = (EmailService)applicationContext.getBean(EmailService.class);
//        emailService.testSyncTask();
        boolean isAop = AopUtils.isAopProxy(EmailController.class);//是否是代理對象;
        boolean isCglib = AopUtils.isCglibProxy(EmailController.class);  //是否是CGLIB方式的代理對象;
        boolean isJdk = AopUtils.isJdkDynamicProxy(EmailController.class);  //是否是JDK動态代理方式的代理對象;
        //以下才是重點!!!
        EmailService emailService = (EmailService)applicationContext.getBean(EmailService.class);
        EmailService proxy = (EmailService) AopContext.currentProxy();
        System.out.println(emailService == proxy ? true : false);
        proxy.testSyncTask();
        System.out.println("end!!!");
    }
}

           

複制

三、異步請求與異步調用的差別

兩者的使用場景不同,異步請求用來解決并發請求對伺服器造成的壓力,進而提高對請求的吞吐量;而異步調用是用來做一些非主線流程且不需要實時計算和響應的任務,比如同步日志到 kafka 中做日志分析等。

異步請求是會一直等待 response 相應的,需要傳回結果給用戶端的;而異步調用我們往往會馬上傳回給用戶端響應,完成這次整個的請求,至于異步調用的任務背景自己慢慢跑就行,用戶端不會關心。