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

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

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

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

  • 什麼是異步請求,有什麼特點,適用場景
  • 四種使用姿勢:
    • AsyncContext 方式
    • Callable
    • WebAsyncTask
    • DeferredResult

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 或者有更好的建議,歡迎批評指正,不吝感激

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

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zlBX6XTG-1585660664211)(https://spring.hhui.top/spring-blog/imgs/info/info.png)]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章