DeferredResult——非同步請求處理

bruce.yao發表於2019-01-17

本文地址: https://www.jianshu.com/p/062c2c6e21da

傳送門


大家都知道,Callable和DeferredResult可以用來進行非同步請求處理。利用它們,我們可以非同步生成返回值,在具體處理的過程中,我們直接在controller中返回相應的Callable或者DeferredResult,在這之後,servlet執行緒將被釋放,可用於其他連線;DeferredResult另外會有執行緒來進行結果處理,並setResult。

基礎準備

在正式開始之前,我們先做一點準備工作,在專案中新建了一個base模組。其中包含一些提供基礎支援的java類,在其他模組中可能會用到。

ResponseMsg

我們定義了一個ResponseMsg的實體類來作為我們的返回值型別:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseMsg<T> {

    private int code;

    private String msg;

    private T data;

}

非常簡單,裡面包含了code、msg和data三個欄位,其中data為泛型型別。另外類的註解Data、NoArgsConstructor和AllArgsConstructor都是lombok提供的簡化我們開發的,主要功能分別是,為我們的類生成set和get方法,生成無參構造器和生成全參構造器。使用idea進行開發的童鞋可以裝一下lombok的支援外掛。另外,lombok的依賴參見:

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-maven</artifactId>
            <version>1.16.16.0</version>
            <type>pom</type>
        </dependency>

TaskService

我們建立了一個TaskService,用來為阻塞呼叫和Callable呼叫提供實際結果處理的。程式碼如下:

@Service
public class TaskService {

    private static final Logger log = LoggerFactory.getLogger(TaskService.class);

    public ResponseMsg<String> getResult(){

        log.info("任務開始執行,持續等待中...");

        try {
            Thread.sleep(30000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("任務處理完成");
        return new ResponseMsg<String>(0,"操作成功","success");
    }
}

可以看到,裡面實際提供服務的是getResult方法,這邊直接返回一個new ResponseMsg<String>(0,"操作成功","success")。但是其中又特意讓它sleep了30秒,模擬一個耗時較長的請求。

阻塞呼叫

平時我們用的最普遍的還是阻塞呼叫,通常請求的處理時間較短,在併發量較小的情況下,使用阻塞呼叫問題也不是很大。
阻塞呼叫實現非常簡單,我們首先新建一個模組blockingtype,裡面只包含一個controller類,用來接收請求並利用TaskService來獲取結果。

@RestController
public class BlockController {

    private static final Logger log = LoggerFactory.getLogger(BlockController.class);

    @Autowired
    private TaskService taskService;

    @RequestMapping(value = "/get", method = RequestMethod.GET)
    public ResponseMsg<String> getResult(){

        log.info("接收請求,開始處理...");
        ResponseMsg<String> result =  taskService.getResult();
        log.info("接收任務執行緒完成並退出");

        return result;

    }
}

我們請求的是getResult方法,其中呼叫了taskService,這個taskService我們是注入得到的。關於怎麼跨模組注入的,其實也非常簡單,在本模組,加入對其他模組的依賴就可以了。比如這裡我們在blockingtype的pom.xml檔案中加入對base模組的依賴:

        <dependency>
            <groupId>com.sunny</groupId>
            <artifactId>base</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

然後我們看一下實際呼叫效果,這裡我們設定埠號為8080,啟動日誌如下:

2018-06-24 19:02:48.514  INFO 11207 --- [           main] com.sunny.BlockApplication               : Starting BlockApplication on xdeMacBook-Pro.local with PID 11207 (/Users/zsunny/IdeaProjects/asynchronoustask/blockingtype/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-24 19:02:48.519  INFO 11207 --- [           main] com.sunny.BlockApplication               : No active profile set, falling back to default profiles: default
2018-06-24 19:02:48.762  INFO 11207 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:02:48 CST 2018]; root of context hierarchy
2018-06-24 19:02:50.756  INFO 11207 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2018-06-24 19:02:50.778  INFO 11207 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2018-06-24 19:02:50.780  INFO 11207 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-24 19:02:50.922  INFO 11207 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-06-24 19:02:50.922  INFO 11207 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2200 ms
2018-06-24 19:02:51.156  INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-24 19:02:51.162  INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-24 19:02:51.163  INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-24 19:02:51.163  INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-24 19:02:51.163  INFO 11207 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-24 19:02:51.620  INFO 11207 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:02:48 CST 2018]; root of context hierarchy
2018-06-24 19:02:51.724  INFO 11207 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public com.sunny.entity.ResponseMsg<java.lang.String> com.sunny.controller.BlockController.getResult()
2018-06-24 19:02:51.730  INFO 11207 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-24 19:02:51.731  INFO 11207 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-24 19:02:51.780  INFO 11207 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:02:51.780  INFO 11207 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:02:51.838  INFO 11207 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:02:52.126  INFO 11207 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-24 19:02:52.205  INFO 11207 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-06-24 19:02:52.211  INFO 11207 --- [           main] com.sunny.BlockApplication               : Started BlockApplication in 5.049 seconds (JVM running for 6.118)

可以看到順利啟動了,那麼我們就來訪問一下:

http://localhost:8080/get

等待了大概30秒左右,得到json資料:

{"code":0,"msg":"操作成功","data":"success"}

圖片.png

 

然後我們來看看控制檯的日誌:

2018-06-24 19:04:07.315  INFO 11207 --- [nio-8080-exec-1] com.sunny.controller.BlockController     : 接收請求,開始處理...
2018-06-24 19:04:07.316  INFO 11207 --- [nio-8080-exec-1] com.sunny.service.TaskService            : 任務開始執行,持續等待中...
2018-06-24 19:04:37.322  INFO 11207 --- [nio-8080-exec-1] com.sunny.service.TaskService            : 任務處理完成
2018-06-24 19:04:37.322  INFO 11207 --- [nio-8080-exec-1] com.sunny.controller.BlockController     : 接收任務執行緒完成並退出

可以看到在“ResponseMsg<String> result = taskService.getResult();”的時候是阻塞了大約30秒鐘,隨後才執行它後面的列印語句“log.info("接收任務執行緒完成並退出");”。

Callable非同步呼叫

涉及到較長時間的請求處理的話,比較好的方式是用非同步呼叫,比如利用Callable返回結果。非同步主要表現在,接收請求的servlet可以不用持續等待結果產生,而可以被釋放去處理其他事情。當然,在呼叫者來看的話,其實還是表現在持續等待30秒。這有利於服務端提供更大的併發處理量。
這裡我們新建一個callabledemo模組,在這個模組中,我們一樣只包含一個TaskController,另外也是需要加入base模組的依賴。只不過這裡我們的返回值不是ResponseMsg型別了,而是一個Callable型別。

@RestController
public class TaskController {

    private static final Logger log = LoggerFactory.getLogger(TaskController.class);

    @Autowired
    private TaskService taskService;

    @RequestMapping(value = "/get",method = RequestMethod.GET)
    public Callable<ResponseMsg<String>> getResult(){

        log.info("接收請求,開始處理...");

        Callable<ResponseMsg<String>> result = (()->{
            return taskService.getResult();
        });

        log.info("接收任務執行緒完成並退出");

        return result;
    }

}

在裡面,我們建立了一個Callable型別的變數result,並實現了它的call方法,在call方法中,我們也是呼叫taskService的getResult方法得到返回值並返回。
下一步我們就執行一下這個模組,這裡我們在模組的application.yml中設定埠號為8081:

server:
  port: 8081

啟動,可以看到控制檯的訊息:

2018-06-24 19:38:14.658  INFO 11226 --- [           main] com.sunny.CallableApplication            : Starting CallableApplication on xdeMacBook-Pro.local with PID 11226 (/Users/zsunny/IdeaProjects/asynchronoustask/callabledemo/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-24 19:38:14.672  INFO 11226 --- [           main] com.sunny.CallableApplication            : No active profile set, falling back to default profiles: default
2018-06-24 19:38:14.798  INFO 11226 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:38:14 CST 2018]; root of context hierarchy
2018-06-24 19:38:16.741  INFO 11226 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8081 (http)
2018-06-24 19:38:16.762  INFO 11226 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2018-06-24 19:38:16.764  INFO 11226 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-24 19:38:16.918  INFO 11226 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-06-24 19:38:16.919  INFO 11226 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2126 ms
2018-06-24 19:38:17.144  INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-24 19:38:17.149  INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-24 19:38:17.150  INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-24 19:38:17.150  INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-24 19:38:17.150  INFO 11226 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-24 19:38:17.632  INFO 11226 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@4445629: startup date [Sun Jun 24 19:38:14 CST 2018]; root of context hierarchy
2018-06-24 19:38:17.726  INFO 11226 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public java.util.concurrent.Callable<com.sunny.entity.ResponseMsg<java.lang.String>> com.sunny.controller.TaskController.getResult()
2018-06-24 19:38:17.731  INFO 11226 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-24 19:38:17.733  INFO 11226 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-24 19:38:17.777  INFO 11226 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:38:17.777  INFO 11226 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:38:17.825  INFO 11226 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 19:38:18.084  INFO 11226 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-24 19:38:18.176  INFO 11226 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http)
2018-06-24 19:38:18.183  INFO 11226 --- [           main] com.sunny.CallableApplication            : Started CallableApplication in 4.538 seconds (JVM running for 5.327)

完美啟動了,然後我們還是一樣,訪問一下:

http://localhost:8081/get

在大約等待了30秒左右,我們在瀏覽器上得到json資料:

{"code":0,"msg":"操作成功","data":"success"}

圖片.png

 

和阻塞呼叫的結果一樣——當然一樣啦,都是同taskService中得到的結果。
然後我們看看控制檯的訊息:

2018-06-24 19:39:07.738  INFO 11226 --- [nio-8081-exec-1] com.sunny.controller.TaskController      : 接收請求,開始處理...
2018-06-24 19:39:07.740  INFO 11226 --- [nio-8081-exec-1] com.sunny.controller.TaskController      : 接收任務執行緒完成並退出
2018-06-24 19:39:07.753  INFO 11226 --- [      MvcAsync1] com.sunny.service.TaskService            : 任務開始執行,持續等待中...
2018-06-24 19:39:37.756  INFO 11226 --- [      MvcAsync1] com.sunny.service.TaskService            : 任務處理完成

很顯然,這裡的訊息出現的順序和阻塞模式有所不同了,這裡在“接收請求,開始處理...”之後直接列印了“接收任務執行緒完成並退出”。而不是先出現“任務處理完成”後再出現“接收任務執行緒完成並退出”。這就說明,這裡沒有阻塞在從taskService中獲得資料的地方,controller中直接執行後面的部分(這裡可以做其他很多事,不僅僅是列印日誌)。

DeferredResult非同步呼叫

前面鋪墊了那麼多,還是主要來說DeferredResult的;和Callable一樣,DeferredResult也是為了支援非同步呼叫。兩者的主要差異,Sunny覺得主要在DeferredResult需要自己用執行緒來處理結果setResult,而Callable的話不需要我們來維護一個結果處理執行緒。總體來說,Callable的話更為簡單,同樣的也是因為簡單,靈活性不夠;相對地,DeferredResult更為複雜一些,但是又極大的靈活性。在可以用Callable的時候,直接用Callable;而遇到Callable沒法解決的場景的時候,可以嘗試使用DeferredResult。
這裡Sunny將會設計兩個DeferredResult使用場景。

場景一:

  1. 建立一個持續在隨機間隔時間後從任務佇列中獲取任務的執行緒
  2. 訪問controller中的方法,建立一個DeferredResult,設定超時時間和超時返回物件
  3. 設定DeferredResult的超時回撥方法和完成回撥方法
  4. 將DeferredResult放入任務中,並將任務放入任務佇列
  5. 步驟1中的執行緒獲取到任務佇列中的任務,併產生一個隨機結果返回
    場景其實非常簡單,接下來我們來看看具體的實現。首先,我們還是來看任務實體類是怎麼樣的。
/**
 * 任務實體類
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {

    private int taskId;

    private DeferredResult<ResponseMsg<String>> taskResult;

    @Override
    public String toString() {
        return "Task{" +
                "taskId=" + taskId +
                ", taskResult" + "{responseMsg=" + taskResult.getResult() + "}" +
                '}';
    }
}

看起來非常簡單,成員變數又taskId和taskResult,前者是int型別,後者為我們的DeferredResult型別,它的泛型型別為ResponseMsg<String>,注意這裡用到ResponseMsg,所以也需要匯入base模組的依賴。另外註解之前已經說明了,不過這裡再提一句,@Data註解也包含了toString的重寫,但是這裡為了知道具體的ResponseMsg的內容,Sunny特意手動重寫。
看完Task型別,我們再來看看任務佇列。

@Component
public class TaskQueue {

    private static final Logger log = LoggerFactory.getLogger(TaskQueue.class);

    private static final int QUEUE_LENGTH = 10;

    private BlockingQueue<Task> queue = new LinkedBlockingDeque<>(QUEUE_LENGTH);

    private int taskId = 0;


    /**
     * 加入任務
     * @param deferredResult
     */
    public void put(DeferredResult<ResponseMsg<String>> deferredResult){

        taskId++;

        log.info("任務加入佇列,id為:{}",taskId);

        queue.offer(new Task(taskId,deferredResult));

    }

    /**
     * 獲取任務
     * @return
     * @throws InterruptedException
     */
    public Task take() throws InterruptedException {

        Task task = queue.poll();

        log.info("獲得任務:{}",task);

        return task;
    }
}

這裡我們將它作為一個bean,之後會在其他bean中注入,這裡實際的佇列為成員變數queue,它是LinkedBlockingDeque型別的。還有一個成員變數為taskId,是用於自動生成任務id的,並且在加入任務的方法中實現自增,以確保每個任務的id唯一性。方法的話又put和take方法,分別用於向佇列中新增任務和取出任務;其中,對queue的操作,分別用了offer和poll,這樣是實現一個非阻塞的操作,並且在佇列為空和佇列已滿的情況下不會丟擲異常。另外,大家實現的時候,可以考慮使用ConcurrentLinkedQueue來高效處理併發,Sunny這裡選擇阻塞佇列,在使用的時候需要加鎖。
然後我們來看步驟1中的,啟動一個持續從任務佇列中獲取任務的執行緒的具體實現。

@Component
public class TaskExecute {

    private static final Logger log = LoggerFactory.getLogger(TaskExecute.class);

    private static final Random random = new Random();

    //預設隨機結果的長度
    private static final int DEFAULT_STR_LEN = 10;
    
    //用於生成隨機結果
    private static final String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

    @Autowired
    private TaskQueue taskQueue;


    /**
     * 初始化啟動
     */
    @PostConstruct
    public void init(){

        log.info("開始持續處理任務");

        new Thread(this::execute).start();


    }


    /**
     * 持續處理
     * 返回執行結果
     */
    private void execute(){

        while (true){

            try {

                //取出任務
                Task task;

                synchronized (taskQueue) {

                    task = taskQueue.take();

                }

                if(task != null) {

                    //設定返回結果
                    String randomStr = getRandomStr(DEFAULT_STR_LEN);

                    ResponseMsg<String> responseMsg = new ResponseMsg<String>(0, "success", randomStr);

                    log.info("返回結果:{}", responseMsg);

                    task.getTaskResult().setResult(responseMsg);
                }

                int time = random.nextInt(10);

                log.info("處理間隔:{}秒",time);

                Thread.sleep(time*1000L);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }

    }

    /**
     * 獲取長度為len的隨機串
     * @param len
     * @return
     */
    private String getRandomStr(int len){

        int maxInd = str.length();

        StringBuilder sb = new StringBuilder();

        int ind;

        for(int i=0;i<len;i++){

            ind = random.nextInt(maxInd);

            sb.append(str.charAt(ind));

        }

        return String.valueOf(sb);

    }
}

這裡,我們注入了TaskQueue,成員變數比較簡單並且有註釋,不再說明,主要來看方法。先看一下最後一個方法getRandomStr,很顯然,這是一個獲得長度為len的隨機串的方法,訪問限定為private,為類中其他方法服務的。然後我們看init方法,它執行的其實就是開啟了一個執行緒並且執行execute方法,注意一下它上面的@PostContruct註解,這個註解就是在這個bean初始化的時候就執行這個方法。所以我們需要關注的實際邏輯在execute方法中。可以看到,在execute方法中,用了一個while(true)來保證執行緒持續執行。因為是併發環境下,考慮對taskQueue加鎖,從中取出任務;如果任務不為空,獲取用getRandomStr生成一個隨機結果並用setResult方法進行返回。最後可以看到,利用random生成來一個[0,10)的隨機數,並讓執行緒sleep相應的秒數。這裡注意一下,需要設定一個時間間隔,否則,先執行緒持續跑會出現CPU負載過高的情況。
接下來我們就看看controller是如何處理的。

@RestController
public class TaskController {

    private static final Logger log = LoggerFactory.getLogger(TaskController.class);

    //超時結果
    private static final ResponseMsg<String> OUT_OF_TIME_RESULT = new ResponseMsg<>(-1,"超時","out of time");

    //超時時間
    private static final long OUT_OF_TIME = 3000L;

    @Autowired
    private TaskQueue taskQueue;


    @RequestMapping(value = "/get",method = RequestMethod.GET)
    public DeferredResult<ResponseMsg<String>> getResult() {

        log.info("接收請求,開始處理...");

        //建立DeferredResult物件,設定超時時間,以及超時返回超時結果
        DeferredResult<ResponseMsg<String>> result = new DeferredResult<>(OUT_OF_TIME, OUT_OF_TIME_RESULT);

        result.onTimeout(() -> {
            log.info("呼叫超時");
        });

        result.onCompletion(() -> {
            log.info("呼叫完成");
        });

        //併發,加鎖
        synchronized (taskQueue) {

            taskQueue.put(result);

        }

        log.info("接收任務執行緒完成並退出");

        return result;

    }

}

這裡我們同樣注入了taskQueue。請求方法就只有一個getResult,返回值為DeferredResult<ResponseMsg<String>>。這裡我們首先建立了DeferredResult物件result並且設定超時時間和超時返回結果;隨後設定result的onTimeout和onCompletion方法,其實就是傳入兩個Runnable物件來實現回撥的效果;之後就是加鎖並且將result加入任務佇列中。
總體來說,場景不算非常複雜,看到這裡大家應該都能基本瞭解了。然後我們來跑一下測試一下。我們在application.yml中設定埠為8082:

server:
  port: 8082

啟動模組,控制檯資訊如下:

2018-06-24 21:49:28.815  INFO 11322 --- [           main] com.sunny.DeferredResultApplication      : Starting DeferredResultApplication on xdeMacBook-Pro.local with PID 11322 (/Users/zsunny/IdeaProjects/asynchronoustask/deferredresultdemo/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-24 21:49:28.821  INFO 11322 --- [           main] com.sunny.DeferredResultApplication      : No active profile set, falling back to default profiles: default
2018-06-24 21:49:29.010  INFO 11322 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5ccddd20: startup date [Sun Jun 24 21:49:28 CST 2018]; root of context hierarchy
2018-06-24 21:49:30.971  INFO 11322 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8082 (http)
2018-06-24 21:49:30.980  INFO 11322 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2018-06-24 21:49:30.981  INFO 11322 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-24 21:49:31.062  INFO 11322 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-06-24 21:49:31.063  INFO 11322 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2066 ms
2018-06-24 21:49:31.207  INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-24 21:49:31.212  INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-24 21:49:31.213  INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-24 21:49:31.213  INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-24 21:49:31.213  INFO 11322 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-24 21:49:31.247  INFO 11322 --- [           main] com.sunny.bean.TaskExecute               : 開始持續處理任務
2018-06-24 21:49:31.249  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskQueue                 : 獲得任務:null
2018-06-24 21:49:31.250  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskExecute               : 處理間隔:6秒
2018-06-24 21:49:31.498  INFO 11322 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5ccddd20: startup date [Sun Jun 24 21:49:28 CST 2018]; root of context hierarchy
2018-06-24 21:49:31.572  INFO 11322 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public org.springframework.web.context.request.async.DeferredResult<com.sunny.entity.ResponseMsg<java.lang.String>> com.sunny.controller.TaskController.getResult()
2018-06-24 21:49:31.576  INFO 11322 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-24 21:49:31.577  INFO 11322 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-24 21:49:31.602  INFO 11322 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 21:49:31.602  INFO 11322 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 21:49:31.628  INFO 11322 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-24 21:49:31.811  INFO 11322 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-24 21:49:31.892  INFO 11322 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8082 (http)
2018-06-24 21:49:31.897  INFO 11322 --- [           main] com.sunny.DeferredResultApplication      : Started DeferredResultApplication in 3.683 seconds (JVM running for 4.873)
2018-06-24 21:49:37.254  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskQueue                 : 獲得任務:null
2018-06-24 21:49:37.254  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskExecute               : 處理間隔:6秒

首先程式完美啟動,這沒有問題,然後我們注意這幾條資訊:

2018-06-24 21:49:31.247  INFO 11322 --- [           main] com.sunny.bean.TaskExecute               : 開始持續處理任務
2018-06-24 21:49:31.249  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskQueue                 : 獲得任務:null
2018-06-24 21:49:31.250  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskExecute               : 處理間隔:6秒

這說明我們TaskExecute中已經成功啟動了持續獲取任務的執行緒。
接著,我們還是訪問一下:

http://localhost:8082/get

這一回等待了若干秒就出現了結果:

{"code":0,"msg":"success","data":"CEUO2lmMJr"}

圖片.png

 

可以看到我們的隨機串是CEUO2lmMJr。再一次請求又會出現不同的隨機串。再看一下我們控制檯的相關資訊:

2018-06-24 21:51:04.303  INFO 11322 --- [nio-8082-exec-1] com.sunny.controller.TaskController      : 接收請求,開始處理...
2018-06-24 21:51:04.304  INFO 11322 --- [nio-8082-exec-1] com.sunny.bean.TaskQueue                 : 任務加入佇列,id為:1
2018-06-24 21:51:04.304  INFO 11322 --- [nio-8082-exec-1] com.sunny.controller.TaskController      : 接收任務執行緒完成並退出
2018-06-24 21:51:04.323  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskQueue                 : 獲得任務:Task{taskId=1, taskResult{responseMsg=null}}
2018-06-24 21:51:04.323  INFO 11322 --- [       Thread-8] com.sunny.bean.TaskExecute               : 返回結果:ResponseMsg(code=0, msg=success, data=CEUO2lmMJr)

也是符合我們的預期,請求進來進入佇列中,由TaskExecute獲取請求並進行處理結果返回。

場景二

  1. 使用者傳送請求到TaskController的getResult方法,該方法接收到請求,建立一個DeferredResult,設定超時時間和超時返回物件
  2. 設定DeferredResult的超時回撥方法和完成回撥方法,超時和完成都會將本次請求產生的DeferredResult從集合中remove
  3. 將DeferredResult放入集合中
  4. 另有一個TaskExecuteController,訪問其中一個方法,可取出集合中的等待返回的DeferredResult物件,並將傳入的引數設定為結果
    首先我們來看看DeferredResult的集合類:
@Component
@Data
public class TaskSet {

    private Set<DeferredResult<ResponseMsg<String>>> set = new HashSet<>();

}

非常簡單,只包含了一個HashSet的成員變數。這裡可以考慮用ConcurrentHashMap來實現高效併發,Sunny這裡簡單實用HashSet,配合加鎖實現併發處理。
然後我們看看發起呼叫的Controller程式碼:

@RestController
public class TaskController {

    private Logger log = LoggerFactory.getLogger(TaskController.class);

    //超時結果
    private static final ResponseMsg<String> OUT_OF_TIME_RESULT = new ResponseMsg<>(-1,"超時","out of time");

    //超時時間
    private static final long OUT_OF_TIME = 60000L;

    @Autowired
    private TaskSet taskSet;

    @RequestMapping(value = "/get",method = RequestMethod.GET)
    public DeferredResult<ResponseMsg<String>> getResult(){

        log.info("接收請求,開始處理...");

        //建立DeferredResult物件,設定超時時間,以及超時返回超時結果
        DeferredResult<ResponseMsg<String>> result = new DeferredResult<>(OUT_OF_TIME, OUT_OF_TIME_RESULT);

        result.onTimeout(() -> {
            log.info("呼叫超時,移除任務,此時佇列長度為{}",taskSet.getSet().size());
            
            synchronized (taskSet.getSet()) {

                taskSet.getSet().remove(result);
            }
        });

        result.onCompletion(() -> {
            log.info("呼叫完成,移除任務,此時佇列長度為{}",taskSet.getSet().size());
            
            synchronized (taskSet.getSet()) {

                taskSet.getSet().remove(result);
            }
        });

        //併發,加鎖
        synchronized (taskSet.getSet()) {

            taskSet.getSet().add(result);

        }
        log.info("加入任務集合,集合大小為:{}",taskSet.getSet().size());

        log.info("接收任務執行緒完成並退出");

        return result;

    }
}

和場景一中有些類似,但是注意這裡在onTimeout和onCompletion中都多了一個移除元素的操作,這也就是每次呼叫結束,需要將集合中的DeferredResult物件移除,即集合中儲存的都是等待請求結果的DeferredResult物件。
然後我們看處理請求結果的Controller:

@RestController
public class TaskExecuteController {

    private static final Logger log = LoggerFactory.getLogger(TaskExecuteController.class);

    @Autowired
    private TaskSet taskSet;

    @RequestMapping(value = "/set/{result}",method = RequestMethod.GET)
    public String setResult(@PathVariable("result") String result){

        ResponseMsg<String> res = new ResponseMsg<>(0,"success",result);

        log.info("結果處理開始,得到輸入結果為:{}",res);

        Set<DeferredResult<ResponseMsg<String>>> set = taskSet.getSet();



        synchronized (set){

            set.forEach((deferredResult)->{deferredResult.setResult(res);});

        }

        return "Successfully set result: " + result;
    }
}

看起來非常簡單,只是做了兩個操作,接收得到的引數並利用引數生成一個ResponseMsg<String>物件,隨後將集合中的所有DeferredResult都設定結果為根據引數生成的ResponseMsg<String>物件。最後返回一個提示:成功設定結果...
好了,話不多說,我們來啟動測試驗證一下。我們說一下驗證的過程,我們同時開啟兩個請求,然後再設定一個結果,最後兩個請求都會得到這個結果。當然同時多個或者一個請求也是一樣。這裡有一個地方需要注意一下:

瀏覽器可能會對相同的url請求有快取策略,也就是同時兩個標籤向同一個url傳送請求,瀏覽器只會先傳送一個請求,等一個請求結束才會再傳送另外一個請求。

這樣,我們考慮從兩個瀏覽器中傳送請求:

localhost:8083/get

然後隨便找其中一個,傳送請求來設定結果:

http://localhost:8083/set/aaa

首先我們先啟動模組,可以從控制檯中看到完美啟動管理了:

2018-06-25 00:18:44.379  INFO 12688 --- [           main] com.sunny.DeferredResultApplication      : Starting DeferredResultApplication on xdeMacBook-Pro.local with PID 12688 (/Users/zsunny/IdeaProjects/asynchronoustask/deferredresultdemo2/target/classes started by zsunny in /Users/zsunny/IdeaProjects/asynchronoustask)
2018-06-25 00:18:44.382  INFO 12688 --- [           main] com.sunny.DeferredResultApplication      : No active profile set, falling back to default profiles: default
2018-06-25 00:18:44.489  INFO 12688 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@96def03: startup date [Mon Jun 25 00:18:44 CST 2018]; root of context hierarchy
2018-06-25 00:18:45.650  INFO 12688 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8083 (http)
2018-06-25 00:18:45.658  INFO 12688 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2018-06-25 00:18:45.659  INFO 12688 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23
2018-06-25 00:18:45.722  INFO 12688 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-06-25 00:18:45.723  INFO 12688 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1241 ms
2018-06-25 00:18:45.817  INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2018-06-25 00:18:45.821  INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-25 00:18:45.821  INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-25 00:18:45.821  INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-25 00:18:45.821  INFO 12688 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-25 00:18:46.150  INFO 12688 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@96def03: startup date [Mon Jun 25 00:18:44 CST 2018]; root of context hierarchy
2018-06-25 00:18:46.197  INFO 12688 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/get],methods=[GET]}" onto public org.springframework.web.context.request.async.DeferredResult<com.sunny.entity.ResponseMsg<java.lang.String>> com.sunny.controller.TaskController.getResult()
2018-06-25 00:18:46.199  INFO 12688 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/set/{result}],methods=[GET]}" onto public java.lang.String com.sunny.controller.TaskExecuteController.setResult(java.lang.String)
2018-06-25 00:18:46.202  INFO 12688 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-06-25 00:18:46.202  INFO 12688 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-06-25 00:18:46.237  INFO 12688 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-25 00:18:46.238  INFO 12688 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-25 00:18:46.262  INFO 12688 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-25 00:18:46.362  INFO 12688 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2018-06-25 00:18:46.467  INFO 12688 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8083 (http)
2018-06-25 00:18:46.472  INFO 12688 --- [           main] com.sunny.DeferredResultApplication      : Started DeferredResultApplication in 2.675 seconds (JVM running for 3.623)

完美啟動,接下來Sunny在火狐中發起一個請求

 

圖片.png

 

可以看到正在等待請求結果。隨後我們在谷歌瀏覽器中發起請求

 

圖片.png


兩個請求同時處於等待狀態,這時候我們看一下控制檯資訊:

2018-06-25 00:22:34.642  INFO 12688 --- [nio-8083-exec-6] com.sunny.controller.TaskController      : 接收請求,開始處理...
2018-06-25 00:22:34.642  INFO 12688 --- [nio-8083-exec-6] com.sunny.controller.TaskController      : 加入任務集合,集合大小為:1
2018-06-25 00:22:34.642  INFO 12688 --- [nio-8083-exec-6] com.sunny.controller.TaskController      : 接收任務執行緒完成並退出
2018-06-25 00:22:37.332  INFO 12688 --- [nio-8083-exec-7] com.sunny.controller.TaskController      : 接收請求,開始處理...
2018-06-25 00:22:37.332  INFO 12688 --- [nio-8083-exec-7] com.sunny.controller.TaskController      : 加入任務集合,集合大小為:2
2018-06-25 00:22:37.332  INFO 12688 --- [nio-8083-exec-7] com.sunny.controller.TaskController      : 接收任務執行緒完成並退出

可以看到兩個請求都已經接收到了,並且加入了佇列。這時候,我們再傳送一個設定結果的請求。

 

圖片.png

 

隨後我們檢視兩個呼叫請求的頁面,發現頁面已經不在等待狀態中了,都已經得到了結果。

 

圖片.png

 

圖片.png


另外,再給大家展示一下超時的結果,即我們發起呼叫請求,但是不發起設定結果的請求,等待時間結束。

圖片.png

 

檢視控制檯資訊:

2018-06-25 00:26:15.898  INFO 12688 --- [nio-8083-exec-4] com.sunny.controller.TaskController      : 接收請求,開始處理...
2018-06-25 00:26:15.898  INFO 12688 --- [nio-8083-exec-4] com.sunny.controller.TaskController      : 加入任務集合,集合大小為:1
2018-06-25 00:26:15.898  INFO 12688 --- [nio-8083-exec-4] com.sunny.controller.TaskController      : 接收任務執行緒完成並退出
2018-06-25 00:27:16.014  INFO 12688 --- [nio-8083-exec-5] com.sunny.controller.TaskController      : 呼叫超時,移除任務,此時佇列長度為1
2018-06-25 00:27:16.018  INFO 12688 --- [nio-8083-exec-5] com.sunny.controller.TaskController      : 呼叫完成,移除任務,此時佇列長度為0

後記

想要完整程式碼的童鞋,可以點此檢視fork實踐。

相關文章