【SpringBoot WEB系列】非同步請求知識點與使用姿勢小結

小灰灰Blog發表於2020-03-31

【SpringBoot WEB系列】非同步請求知識點與使用姿勢小結

在 Servlet3.0 就引入了非同步請求的支援,但是在實際的業務開發中,可能用過這個特性的童鞋並不多?

本篇博文作為非同步請求的掃盲和使用教程,將包含以下知識點

  • 什麼是非同步請求,有什麼特點,適用場景
  • 四種使用姿勢:

    • AsyncContext 方式
    • Callable
    • WebAsyncTask
    • DeferredResult

<!-- more -->

I. 非同步請求

非同步對於我們而言,應該屬於經常可以聽到的詞彙了,在實際的開發中多多少少都會用到,那麼什麼是非同步請求呢

1. 非同步請求描述

先介紹一下同步與非同步:

一個正常呼叫,吭哧吭哧執行完畢之後直接返回,這個叫同步;

接收到呼叫,自己不幹,新開一個執行緒來做,主執行緒自己則去幹其他的事情,等後臺執行緒吭哧吭哧的跑完之後,主執行緒再返回結果,這個就叫非同步

非同步請求:

我們這裡講到的非同步請求,主要是針對 web 請求而言,後端響應請求的一種手段,同步/非同步對於前端而言是無感知、無區別的

同步請求,後端接收到請求之後,直接在處理請求執行緒中,執行業務邏輯,並返回

來源於網路

非同步請求,後端接收到請求之後,新開一個執行緒,來執行業務邏輯,釋放請求執行緒,避免請求執行緒被大量耗時的請求沾滿,導致服務不可用

來源於網路

2. 特點

透過上面兩張圖,可以知道非同步請求的最主要特點

  • 業務執行緒,處理請求邏輯
  • 請求處理執行緒立即釋放,透過回撥處理執行緒返回結果

3. 場景分析

從特點出發,也可以很容易看出非同步請求,更適用於耗時的請求,快速的釋放請求處理執行緒,避免 web 容器的請求執行緒被打滿,導致服務不可用

舉一個稍微極端一點的例子,比如我以前做過的一個多媒體服務,提供圖片、音影片的編輯,這些服務介面有同步返回結果的也有非同步返回結果的;同步返回結果的介面有快有慢,大部分耗時可能<10ms,而有部分介面耗時則在幾十甚至上百

這種場景下,耗時的介面就可以考慮用非同步請求的方式來支援了,避免佔用過多的請求處理執行緒,影響其他的服務

II. 使用姿勢

接下來介紹四種非同步請求的使用姿勢,原理一致,只是使用的場景稍有不同

1. AsyncContext

在 Servlet3.0+之後就支援了非同步請求,第一種方式比較原始,相當於直接藉助 Servlet 的規範來實現,當然下面的 case 並不是直接建立一個 servlet,而是藉助AsyncContext來實現

@RestController
@RequestMapping(path = "servlet")
public class ServletRest {

    @GetMapping(path = "get")
    public void get(HttpServletRequest request) {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.addListener(new AsyncListener() {
            @Override
            public void onComplete(AsyncEvent asyncEvent) throws IOException {
                System.out.println("操作完成:" + Thread.currentThread().getName());
            }

            @Override
            public void onTimeout(AsyncEvent asyncEvent) throws IOException {
                System.out.println("超時返回!!!");
                asyncContext.getResponse().setCharacterEncoding("utf-8");
                asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
                asyncContext.getResponse().getWriter().println("超時了!!!!");
            }

            @Override
            public void onError(AsyncEvent asyncEvent) throws IOException {
                System.out.println("出現了m某些異常");
                asyncEvent.getThrowable().printStackTrace();

                asyncContext.getResponse().setCharacterEncoding("utf-8");
                asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
                asyncContext.getResponse().getWriter().println("出現了某些異常哦!!!!");
            }

            @Override
            public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
                System.out.println("開始執行");
            }
        });

        asyncContext.setTimeout(3000L);
        asyncContext.start(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Long.parseLong(request.getParameter("sleep")));
                    System.out.println("內部執行緒:" + Thread.currentThread().getName());
                    asyncContext.getResponse().setCharacterEncoding("utf-8");
                    asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
                    asyncContext.getResponse().getWriter().println("非同步返回!");
                    asyncContext.getResponse().getWriter().flush();
                    // 非同步完成,釋放
                    asyncContext.complete();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        System.out.println("主執行緒over!!! " + Thread.currentThread().getName());
    }
}

完整的實現如上,簡單的來看一下一般步驟

  • javax.servlet.ServletRequest#startAsync()獲取AsyncContext
  • 新增監聽器 asyncContext.addListener(AsyncListener)(這個是可選的)

    • 使用者請求開始、超時、異常、完成時回撥
  • 設定超時時間 asyncContext.setTimeout(3000L) (可選)
  • 非同步任務asyncContext.start(Runnable)

2. Callable

相比較於上面的複雜的示例,SpringMVC 可以非常 easy 的實現,直接返回一個Callable即可

@RestController
@RequestMapping(path = "call")
public class CallableRest {

    @GetMapping(path = "get")
    public Callable<String> get() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("do some thing");
                Thread.sleep(1000);
                System.out.println("執行完畢,返回!!!");
                return "over!";
            }
        };

        return callable;
    }


    @GetMapping(path = "exception")
    public Callable<String> exception() {
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("do some thing");
                Thread.sleep(1000);
                System.out.println("出現異常,返回!!!");
                throw new RuntimeException("some error!");
            }
        };

        return callable;
    }
}

請注意上面的兩種 case,一個正常返回,一個業務執行過程中,丟擲來異常

分別請求,輸出如下

# http://localhost:8080/call/get
do some thing
執行完畢,返回!!!

異常請求: http://localhost:8080/call/exception

do some thing
出現異常,返回!!!
2020-03-29 16:12:06.014 ERROR 24084 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] threw exception

java.lang.RuntimeException: some error!
    at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:40) ~[classes/:na]
    at com.git.hui.boot.async.rest.CallableRest$2.call(CallableRest.java:34) ~[classes/:na]
    at org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:328) ~[spring-web-5.2.1.RELEASE.jar:5.2.1.RELEASE]
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) ~[na:1.8.0_171]
    at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) ~[na:1.8.0_171]
    at java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:1.8.0_171]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_171]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_171]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_171]

3. WebAsyncTask

callable 的方式,非常直觀簡單,但是我們經常關注的超時+異常的處理卻不太好,這個時候我們可以用WebAsyncTask,實現姿勢也很簡單,包裝一下callable,然後設定各種回撥事件即可

@RestController
@RequestMapping(path = "task")
public class WebAysncTaskRest {

    @GetMapping(path = "get")
    public WebAsyncTask<String> get(long sleep, boolean error) {
        Callable<String> callable = () -> {
            System.out.println("do some thing");
            Thread.sleep(sleep);

            if (error) {
                System.out.println("出現異常,返回!!!");
                throw new RuntimeException("異常了!!!");
            }

            return "hello world";
        };

        // 指定3s的超時
        WebAsyncTask<String> webTask = new WebAsyncTask<>(3000, callable);
        webTask.onCompletion(() -> System.out.println("over!!!"));

        webTask.onTimeout(() -> {
            System.out.println("超時了");
            return "超時返回!!!";
        });

        webTask.onError(() -> {
            System.out.println("出現異常了!!!");
            return "異常返回";
        });

        return webTask;
    }
}

4. DeferredResult

DeferredResultWebAsyncTask最大的區別就是前者不確定什麼時候會返回結果,

DeferredResult的這個特點,可以用來做實現很多有意思的東西,如後面將介紹的SseEmitter就用到了它

下面給出一個例項

@RestController
@RequestMapping(path = "defer")
public class DeferredResultRest {

    private Map<String, DeferredResult> cache = new ConcurrentHashMap<>();

    @GetMapping(path = "get")
    public DeferredResult<String> get(String id) {
        DeferredResult<String> res = new DeferredResult<>();
        cache.put(id, res);

        res.onCompletion(new Runnable() {
            @Override
            public void run() {
                System.out.println("over!");
            }
        });
        return res;
    }

    @GetMapping(path = "pub")
    public String publish(String id, String content) {
        DeferredResult<String> res = cache.get(id);
        if (res == null) {
            return "no consumer!";
        }

        res.setResult(content);
        return "over!";
    }
}

在上面的例項中,使用者如果先訪問http://localhost:8080/defer/get?id=yihuihui,不會立馬有結果,直到使用者再次訪問http://localhost:8080/defer/pub?id=yihuihui&content=哈哈時,前面的請求才會有結果返回

那麼這個可以設定超時麼,如果一直把前端掛住,貌似也不太合適吧

  • 在構造方法中指定超時時間: new DeferredResult<>(3000L)
  • 設定全域性的預設超時時間
@Configuration
@EnableWebMvc
public class WebConf implements WebMvcConfigurer {

    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 超時時間設定為60s
        configurer.setDefaultTimeout(TimeUnit.SECONDS.toMillis(10));
    }
}

II. 其他

0. 專案

相關博文

系列博文

原始碼

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

一灰灰blog

相關文章