【Java分享客棧】一文搞定京東零售開源的AsyncTool,徹底解決非同步編排問題。

福隆苑居士發表於2022-04-28

一、前言

本章主要是承接上一篇講CompletableFuture的文章,想了解的可以先去看看案例:

https://juejin.cn/post/7091132240574283813

CompletableFuture已經提供了序列、並行等常用非同步編排的方案,但在細節上還是有許多不足,比如回撥方面,編排複雜順序方面,就捉襟見肘了。


之前我有關注過Gitee上star量還不錯的一款開源工具AsyncTool:

https://gitee.com/jd-platform-opensource/asyncTool

是由京東零售的高階工程師編寫的,提供了非常豐富的非同步編排功能,並且經過了京東內部的測試,是對CompletableFuture的封裝和補足,試用了一下挺不錯。


二、用法

1、引入

1)、不推薦:maven引入,這個比較坑,客觀原因經常會導致依賴下載不下來,不推薦使用;

2)、推薦:直接下載原始碼,因為程式碼量很少,就幾個核心類和測試類。

如下圖所示,把下載的原始碼拷貝進來即可,核心程式碼放到java目錄裡面,測試程式碼放到test目錄裡面。

111.jpg


2、編寫worker

1)、worker是AsyncTool中的一個思想,專門來處理任務的,比如查詢、rpc呼叫等耗時操作,一個任務就是一個worker;

2)、構建worker十分簡單,只需要實現IWorker和ICallback介面即可;

3)、這裡,我們承接上一篇文章的案例,分別建立查詢二十四節氣和查詢星座的worker;

4)、其中begin方法是構建開始時會執行,result方法是獲取到結果後會執行,action方法就是處理具體任務的地方,一般業務就在這裡編寫,defaultValue方法提供超時異常時返回的預設值。

1)、二十四節氣worker

package com.example.async.worker;

import cn.hutool.http.HttpUtil;
import com.jd.platform.async.callback.ICallback;
import com.jd.platform.async.callback.IWorker;
import com.jd.platform.async.executor.timer.SystemClock;
import com.jd.platform.async.worker.WorkResult;
import com.jd.platform.async.wrapper.WorkerWrapper;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 二十四節氣worker
 * </p>
 *
 * @author 福隆苑居士,公眾號:【Java分享客棧】
 * @since 2022-04-27 18:01
 */
@Slf4j
public class TwentyFourWorker implements IWorker<String, String>, ICallback<String, String> {

   public static final String APPKEY = "xxxxxx";// 你的appkey
   public static final String URL = "https://api.jisuapi.com/jieqi/query";

   @Override
   public void begin() {
      // System.out.println(Thread.currentThread().getName() + "- start --" + System.currentTimeMillis());
   }

   @Override
   public void result(boolean success, String param, WorkResult<String> workResult) {
      if (success) {
         System.out.println("callback twentyFourWorker success--" + SystemClock.now() + "----" + workResult.getResult()
               + "-threadName:" +Thread.currentThread().getName());
      } else {
         System.err.println("callback twentyFourWorker failure--" + SystemClock.now() + "----"  + workResult.getResult()
               + "-threadName:" +Thread.currentThread().getName());
      }
   }

   /**
    * 查詢二十四節氣
    */
   @Override
   public String action(String object, Map<String, WorkerWrapper> allWrappers) {
      String url = URL + "?appkey=" + APPKEY;
      String result = HttpUtil.get(url);

      // 模擬時長
      try {
         TimeUnit.SECONDS.sleep(5);
      } catch (Exception e) {
         log.error("[二十四節氣]>>>> 異常: {}", e.getMessage(), e);
      }

      return result;
   }

   @Override
   public String defaultValue() {
      return "twentyFourWorker";
   }
}

2)、星座worker

package com.example.async.worker;

import cn.hutool.http.HttpUtil;
import com.jd.platform.async.callback.ICallback;
import com.jd.platform.async.callback.IWorker;
import com.jd.platform.async.executor.timer.SystemClock;
import com.jd.platform.async.worker.WorkResult;
import com.jd.platform.async.wrapper.WorkerWrapper;
import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 星座worker
 * </p>
 *
 * @author 福隆苑居士,公眾號:【Java分享客棧】
 * @since 2022-04-27 18:01
 */
@Slf4j
public class ConstellationWorker implements IWorker<String, String>, ICallback<String, String> {

   public static final String APPKEY = "xxxxxx";// 你的appkey
   public static final String URL = "https://api.jisuapi.com/astro/all";

   @Override
   public void begin() {
      // System.out.println(Thread.currentThread().getName() + "- start --" + System.currentTimeMillis());
   }

   @Override
   public void result(boolean success, String param, WorkResult<String> workResult) {
      if (success) {
         System.out.println("callback constellationWorker success--" + SystemClock.now() + "----" + workResult.getResult()
               + "-threadName:" +Thread.currentThread().getName());
      } else {
         System.err.println("callback constellationWorker failure--" + SystemClock.now() + "----"  + workResult.getResult()
               + "-threadName:" +Thread.currentThread().getName());
      }
   }

   /**
    * 查詢星座
    */
   @Override
   public String action(String object, Map<String, WorkerWrapper> allWrappers) {
      String url = URL + "?appkey=" + APPKEY;
      String result = HttpUtil.get(url);

      // 模擬異常
      //    int i = 1/0;

      // 模擬時長
      try {
         TimeUnit.SECONDS.sleep(5);
      } catch (Exception e) {
         log.error("[星座]>>>> 異常: {}", e.getMessage(), e);
      }

      return result;
   }

   @Override
   public String defaultValue() {
      return "constellationWorker";
   }
}

3、非同步編排

1)、新建一個AsyncToolService,在裡面進行worker的宣告、構建、編排;

2)、Async.beginWork就是執行非同步任務,引數分別為超時時間和worker,其中超時時間可以自己設短一點看效果;

3)、最後封裝結果返回即可,這裡為演示案例節省時間直接用map返回。

package com.example.async.service;

import com.example.async.worker.ConstellationWorker;
import com.example.async.worker.TwentyFourWorker;
import com.jd.platform.async.executor.Async;
import com.jd.platform.async.wrapper.WorkerWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;

/**
 * <p>
 * AsyncTools服務
 * </p>
 *
 * @author 福隆苑居士,公眾號:【Java分享客棧】
 * @since 2022-04-27 17:56
 */
@Service
@Slf4j
public class AsyncToolService {

   /**
    * 非同步返回結果
    *     ---- 方式:AsyncTool並行處理
    *
    * @return 結果
    */
   public Map<String, Object> queryAsync() throws ExecutionException, InterruptedException {
      // 宣告worker
      TwentyFourWorker twentyFourWorker = new TwentyFourWorker();
      ConstellationWorker constellationWorker = new ConstellationWorker();

      // 構建二十四節氣worker
      WorkerWrapper<String, String> twentyFourWrapper =  new WorkerWrapper.Builder<String, String>()
            .worker(twentyFourWorker)
            .callback(twentyFourWorker)
            .param("0")
            .build();

      // 構建星座worker
      WorkerWrapper<String, String> constellationWrapper =  new WorkerWrapper.Builder<String, String>()
            .worker(constellationWorker)
            .callback(constellationWorker)
            .param("1")
            .build();

      // 開始工作,這裡設定超時時間10s,測試時可以設短一點看效果。
      Async.beginWork(10000, twentyFourWrapper, constellationWrapper);

      // 列印當前執行緒數
      log.debug("----------------- 當前執行緒數 ----------------");
      log.debug(Async.getThreadCount());

      // 列印結果
      log.debug("----------------- 二十四節氣 ----------------");
      log.debug("結果: {}", twentyFourWrapper.getWorkResult());
      log.debug("----------------- 星座 ----------------");
      log.debug("結果: {}", constellationWrapper.getWorkResult());

      // 返回
      Map<String, Object> map = new HashMap<>();
      map.put("twentyFour", twentyFourWrapper.getWorkResult());
      map.put("constellation", constellationWrapper.getWorkResult());

      // 關閉(spring web類應用不用關閉,否則第二次執行會報執行緒池異常。)
      // Async.shutDown();

      return map;
   }
}

4、測試效果

上一篇的案例有演示同步執行的結果,在10秒左右,而CompletableFuture的結果在5秒多點。
這裡測試後發現,AsyncTool的結果也是5秒左右,和CompletableFuture差不多,但AsyncTool提供的編排更豐富。

222.jpg

我們把其中一個星座worker的任務耗時調大,模擬一下超時的效果。可以發現,AsyncTool直接返回了我們上面defaultValue方法中設定的預設值。

333.jpg


三、常用編排

AsyncTool其實提供了很豐富的非同步編排方式,包括較複雜的編排,但以我呆過的中小企業為例,幾乎用不到複雜編排,最常用的的還是並行以及序列+並行。

AsyncTool的QuickStart.md已經做了簡潔的說明:https://gitee.com/jd-platform-opensource/asyncTool/blob/master/QuickStart.md

1)、任務並行

也就是本篇我們案例使用的編排,是我個人平時最常用的一種。

444.jpg


2)、序列+並行

這種其實就是通過next()來做序列和並行的銜接,有些場景也會用到。

555.jpg


3)、依賴其他任務的結果

這也是很常見的場景,B任務依賴A任務的結果來實現業務,最後返回。

AsyncTool也提供了很方便的方式:

1)、在service中構建worker-A時設定一個id名稱;

2)、你可以發現worker的action方法第二個入參是個map,裡面就是所有的wrapper;

3)、在worker-B的action方法中get這個id來獲取wrapper從而拿到A的結果即可。

666.jpg

777.jpg


四、避坑經驗

1、勿關閉執行緒池

AsyncTool提供了很多測試類,裡面包含了所有編排方式,可以一一檢視並驗證,但使用過程中要注意一點,如果是spring-web專案,比如springboot,不需要手動Async.shutdown()處理,否則會執行一次後就關閉執行緒池了,這是不少人直接拷貝test程式碼疏忽的地方。

888.jpg


2、自定義執行緒池

這個問題可以在AsyncTool的issue中看到相關討論,作者君是根據京東零售的業務來決定使用什麼執行緒池的,他們使用的預設執行緒池就是newCachedThreadPool,無限制長度的執行緒池,且具備複用特性,按照作者君的說法,因為京東的場景多數為低耗時(10ms)高併發,瞬間衝擊的場景,所以最適合這種執行緒池。

999.jpg

1010.jpg

而根據我的經驗,不同公司的業務和專案都不同,中小企業往往依靠企事業單位生存,對接第三方廠家較多,rpc介面耗時往往較長且不可控,不符合京東零售低耗時高併發的特點,直接使用Integer.MAX_VALUE的無限制核心執行緒數的方式不太合適。

我建議中小企業使用自定義執行緒池,根據自身硬體水平和壓測結果調整最終核心執行緒數和任務佇列長度,確定合適的拒絕策略,比如直接拒絕或走主執行緒,這樣會比較穩妥。


五、示例程式碼

完整示例程式碼提供給大家,裡面有我的極速資料API的key,每天100次免費呼叫,省去註冊賬號,先到先測,慢點就只能等明天了哦。

連結:https://pan.baidu.com/doc/share/kJyph2LX076okHVWv38tlw-159275174957933

提取碼:yqms



原創文章純手打,覺得有點幫助就請點個推薦吧。


不定期分享工作經驗和趣事,喜歡的話就請關注一下吧。


相關文章