SpringBoot異步請求

何爲異步請求

在Servlet 3.0之前,Servlet採用Thread-Per-Request的方式處理請求,即每一次Http請求都由某一個線程從頭到尾負責處理。如果一個請求需要進行IO操作,比如訪問數據庫、調用第三方服務接口等,那麼其所對應的線程將同步地等待****IO操作完成, 而IO操作是非常慢的,所以此時的線程並不能及時地釋放回線程池以供後續使用,在併發量越來越大的情況下,這將帶來嚴重的性能問題。其請求流程大致爲:

而在Servlet3.0發佈後,提供了一個新特性:異步處理請求。可以先釋放容器分配給請求的線程與相關資源,減輕系統負擔,釋放了容器所分配線程的請求,其響應將被延後,可以在耗時處理完成(例如長時間的運算)時再對客戶端進行響應。其請求流程爲:

在Servlet 3.0後,我們可以從HttpServletRequest對象中獲得一個**AsyncContext**對象,該對象構成了異步處理的上下文,Request和Response對象都可從中獲取。AsyncContext可以從當前線程傳給另外的線程,並在新的線程中完成對請求的處理並返回結果給客戶端,初始線程便可以還回給容器線程池以處理更多的請求。如此,通過將請求從一個線程傳給另一個線程處理的過程便構成了Servlet 3.0中的異步處理。

隨着Spring5發佈,提供了一個響應式Web框架:Spring WebFlux。之後可能就不需要Servlet容器的支持了。以下是其先後對比圖:

左側是傳統的基於Servlet的Spring Web MVC框架,右側是5.0版本新引入的基於Reactive Streams的Spring WebFlux框架,從上到下依次是Router Functions,WebFlux,Reactive Streams三個新組件。

原生異步請求API說明

在編寫實際代碼之前,我們來了解下一些關於異步請求的api的調用說明。

  • 獲取AsyncContext:根據HttpServletRequest對象獲取。

AsyncContext asyncContext = request.startAsync();

  • 設置監聽器:可設置其開始、完成、異常、超時等事件的回調處理

其監聽器的接口代碼:

public interface AsyncListener extends EventListener {
    void onComplete(AsyncEvent event) throws IOException;
    void onTimeout(AsyncEvent event) throws IOException;
    void onError(AsyncEvent event) throws IOException;
    void onStartAsync(AsyncEvent event) throws IOException;
}

說明:

  1. onStartAsync:異步線程開始時調用
  2. onError:異步線程出錯時調用
  3. onTimeout:異步線程執行超時調用
  4. onComplete:異步執行完畢時調用

一般上,我們在超時或者異常時,會返回給前端相應的提示,比如說超時了,請再次請求等等,根據各業務進行自定義返回。同時,在異步調用完成時,一般需要執行一些清理工作或者其他相關操作。

需要注意的是只有在調用request.startAsync前將監聽器添加到AsyncContext,監聽器的onStartAsync方法纔會起作用,而調用startAsync前AsyncContext還不存在,所以第一次調用startAsync是不會被監聽器中的onStartAsync方法捕獲的,只有在超時後又重新開始的情況下onStartAsync方法纔會起作用。

  • 設置超時:通過setTimeout方法設置,單位:毫秒。

一定要設置超時時間,不能無限等待下去,不然和正常的請求就一樣了。。

Servlet方式實現異步請求

前面已經提到,可通過HttpServletRequest對象中獲得一個**AsyncContext**對象,該對象構成了異步處理的上下文。所以,我們來實際操作下。

1、編寫一個簡單控制層

/**
 * 使用servlet方式進行異步請求
 *
 */
@Slf4j
@RestController
public class ServletController {
    
    @RequestMapping("/servlet/orig")
    public void todo(HttpServletRequest request,HttpServletResponse response) throws Exception {
        //這裏來個休眠
        Thread.sleep(100);
        response.getWriter().println("這是【正常】的請求返回");
    }
    
    @RequestMapping("/servlet/async")
    public void todoAsync(HttpServletRequest request,HttpServletResponse response) {
        AsyncContext asyncContext = request.startAsync();
        asyncContext.addListener(new AsyncListener() {
            
            @Override
            public void onTimeout(AsyncEvent event) throws IOException {
                log.info("超時了:");
                //做一些超時後的相關操作
            }
            
            @Override
            public void onStartAsync(AsyncEvent event) throws IOException {
                // TODO Auto-generated method stub
                log.info("線程開始");
            }
            
            @Override
            public void onError(AsyncEvent event) throws IOException {
                log.info("發生錯誤:",event.getThrowable());
            }
            
            @Override
            public void onComplete(AsyncEvent event) throws IOException {
                log.info("執行完成");
                //這裏可以做一些清理資源的操作
                
            }
        });
        //設置超時時間
        asyncContext.setTimeout(200);
        //也可以不使用start 進行異步調用
//        new Thread(new Runnable() {
//            
//            @Override
//            public void run() {
//                編寫業務邏輯
//                
//            }
//        }).start();
        
        asyncContext.start(new Runnable() {            
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                    log.info("內部線程:" + Thread.currentThread().getName());
                    asyncContext.getResponse().setCharacterEncoding("utf-8");
                    asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
                    asyncContext.getResponse().getWriter().println("這是【異步】的請求返回");
                } catch (Exception e) {
                    log.error("異常:",e);
                }
                //異步請求完成通知
                //此時整個請求才完成
                //其實可以利用此特性 進行多條消息的推送 把連接掛起。。
                asyncContext.complete();
            }
        });
        //此時之類 request的線程連接已經釋放了
        log.info("線程:" + Thread.currentThread().getName());
    }

}

注意:異步請求時,可以利用ThreadPoolExecutor自定義個線程池。

1.啓動下應用,查看控制檯輸出就可以獲悉是否在同一個線程裏面了。同時,可設置下等待時間,之後就會調用超時回調方法了。

使用過濾器時,需要加入asyncSupported爲true配置,開啓異步請求支持。

@WebServlet(urlPatterns = "/okong", asyncSupported = true )  
public  class AsyncServlet extends HttpServlet ...

題外話:其實我們可以利用在未執行asyncContext.complete()方法時請求未結束這特性,可以做個簡單的文件上傳進度條之類的功能。但注意請求是會超時的,需要設置超時的時間下。

Spring方式實現異步請求

在Spring中,有多種方式實現異步請求,比如callable、DeferredResult或者WebAsyncTask。每個的用法略有不同,可根據不同的業務場景選擇不同的方式。以下主要介紹一些常用的用法

Callable

使用很簡單,直接返回的參數包裹一層callable即可。

用法

    @RequestMapping("/callable")
    public Callable<String> callable() {
        log.info("外部線程:" + Thread.currentThread().getName());
        return new Callable<String>() {

            @Override
            public String call() throws Exception {
                log.info("內部線程:" + Thread.currentThread().getName());
                return "callable!";
            }
        };
    }

控制檯輸出:

 

超時、自定義線程設置

從控制檯可以看見,異步響應的線程使用的是名爲:MvcAsync1的線程。第一次再訪問時,就是MvcAsync2了。若採用默認設置,會無限的創建新線程去處理異步請求,所以正常都需要配置一個線程池及超時時間。

編寫一個配置類

@Configuration
public class JavaConfig {
    /**
     * 配置線程池
     * @return
     */
    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //此方法返回可用處理器的虛擬機的最大數量; 不小於1
        int core = Runtime.getRuntime().availableProcessors();
        taskExecutor.setCorePoolSize(core);
        taskExecutor.setMaxPoolSize(core*2 + 1);
        taskExecutor.setQueueCapacity(25);
        taskExecutor.setKeepAliveSeconds(200);
        taskExecutor.setThreadNamePrefix("callable-");//線程名稱前綴
        // 線程池對拒絕任務(無線程可用)的處理策略,目前只支持AbortPolicy、CallerRunsPolicy;默認爲後者
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
}

DeferredResult

相比於callable,DeferredResult可以處理一些相對複雜一些的業務邏輯,最主要還是可以在另一個線程裏面進行業務處理及返回,即可在兩個完全不相干的線程間的通信。

/**
 * 線程池
 */
public static ExecutorService FIXED_THREAD_POOL = Executors.newFixedThreadPool(30);

@RequestMapping("/deferredresult")
public DeferredResult<String> deferredResult(){
    log.info("外部線程:" + Thread.currentThread().getName());
    //設置超時時間
    DeferredResult<String> result = new DeferredResult<String>(60*1000L);
    //處理超時事件 採用委託機制
    result.onTimeout(new Runnable() {

        @Override
        public void run() {
            log.error("DeferredResult超時");
            result.setResult("超時了!");
        }
    });
    result.onCompletion(new Runnable() {

        @Override
        public void run() {
            //完成後
            log.info("調用完成");
        }
    });
    FIXED_THREAD_POOL.execute(new Runnable() {

        @Override
        public void run() {
            //處理業務邏輯
            log.info("內部線程:" + Thread.currentThread().getName());
            //返回結果
            result.setResult("DeferredResult!!");
        }
    });
    return result;
}

注意:返回結果時記得調用下setResult方法。

題外話:利用DeferredResult可實現一些長連接的功能,比如當某個操作是異步時,我們可以保存這個DeferredResult對象,當異步通知回來時,我們在找回這個DeferredResult對象,之後在setResult會結果即可。提高性能。

WebAsyncTask

使用方法都類似,只是WebAsyncTask是直接返回了。

@RequestMapping("/webAsyncTask")
    public WebAsyncTask<String> webAsyncTask() {
        log.info("外部線程:" + Thread.currentThread().getName());
        WebAsyncTask<String> result = new WebAsyncTask<String>(60*1000L, new Callable<String>() {

            @Override
            public String call() throws Exception {
                log.info("內部線程:" + Thread.currentThread().getName());
                return "WebAsyncTask!!!";
            }
        });
        result.onTimeout(new Callable<String>() {
            
            @Override
            public String call() throws Exception {
                // TODO Auto-generated method stub
                return "WebAsyncTask超時!!!";
            }
        });
        result.onCompletion(new Runnable() {
            
            @Override
            public void run() {
                //超時後 也會執行此方法
                log.info("WebAsyncTask執行結束");
            }
        });
        return result;
    }

發佈了151 篇原創文章 · 獲贊 184 · 訪問量 44萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章