DeferredResult异步请求处理 提高系统吞吐量的一把利器

基础准备
responsemsg
taskservice
阻塞调用
callable异步调用
deferredresult异步调用
后记
大家都知道,callable和deferredresult可以用来进行异步请求处理。利用它们,我们可以异步生成返回值,在具体处理的过程中,我们直接在controller中返回相应的callable或者deferredresult,在这之后,servlet线程将被释放,可用于其他连接;deferredresult另外会有线程来进行结果处理,并setresult。
基础准备
在正式开始之前,我们先做一点准备工作,在项目中新建了一个base模块。其中包含一些提供基础支持的java类,在其他模块中可能会用到。
responsemsg
我们定义了一个responsemsg的实体类来作为我们的返回值类型:
@data@noargsconstructor@allargsconstructorpublic class responsemsg {    private int code;    private string msg;    private t data;}  
非常简单,里面包含了code、msg和data三个字段,其中data为泛型类型。另外类的注解data、noargsconstructor和allargsconstructor都是lombok提供的简化我们开发的,主要功能分别是,为我们的类生成set和get方法,生成无参构造器和生成全参构造器。
使用idea进行开发的童鞋可以装一下lombok的支持插件。另外,lombok的依赖参见:
org.projectlombok    lombok-maven    1.16.16.0    pom  
taskservice
我们建立了一个taskservice,用来为阻塞调用和callable调用提供实际结果处理的。代码如下:
@servicepublic class taskservice {    private static final logger log = loggerfactory.getlogger(taskservice.class);    public responsemsg getresult(){        log.info(任务开始执行,持续等待中...);        try {            thread.sleep(30000l);        } catch (interruptedexception e) {            e.printstacktrace();        }        log.info(任务处理完成);        return new responsemsg(0,操作成功,success);    }}  
可以看到,里面实际提供服务的是getresult方法,这边直接返回一个new responsemsg(0,“操作成功”,“success”)。但是其中又特意让它sleep了30秒,模拟一个耗时较长的请求。
阻塞调用
平时我们用的最普遍的还是阻塞调用,通常请求的处理时间较短,在并发量较小的情况下,使用阻塞调用问题也不是很大。 阻塞调用实现非常简单,我们首先新建一个模块blockingtype,里面只包含一个controller类,用来接收请求并利用taskservice来获取结果。
@restcontrollerpublic class blockcontroller {    private static final logger log = loggerfactory.getlogger(blockcontroller.class);    @autowired    private taskservice taskservice;    @requestmapping(value = /get, method = requestmethod.get)    public responsemsg getresult(){        log.info(接收请求,开始处理...);        responsemsg result =  taskservice.getresult();        log.info(接收任务线程完成并退出);        return result;    }}  
我们请求的是getresult方法,其中调用了taskservice,这个taskservice我们是注入得到的。关于怎么跨模块注入的,其实也非常简单,在本模块,加入对其他模块的依赖就可以了。比如这里我们在blockingtype的pom.xml文件中加入对base模块的依赖:
com.sunny    base    1.0-snapshot  
然后我们看一下实际调用效果,这里我们设置端口号为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: default2018-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 hierarchy2018-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 1950.778  info 11207 --- [           main] o.apache.catalina.core.standardservice   : starting service [tomcat]2018-06-24 1950.780  info 11207 --- [           main] org.apache.catalina.core.standardengine  : starting servlet engine: apache tomcat/8.5.232018-06-24 1950.922  info 11207 --- [ost-startstop-1] o.a.c.c.c.[tomcat].[localhost].[/]       : initializing spring embedded webapplicationcontext2018-06-24 1950.922  info 11207 --- [ost-startstop-1] o.s.web.context.contextloader            : root webapplicationcontext: initialization completed in 2200 ms2018-06-24 1951.156  info 11207 --- [ost-startstop-1] o.s.b.w.servlet.servletregistrationbean  : mapping servlet: 'dispatcherservlet' to [/]2018-06-24 1951.162  info 11207 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'characterencodingfilter' to: [/*]2018-06-24 1951.163  info 11207 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'hiddenhttpmethodfilter' to: [/*]2018-06-24 1951.163  info 11207 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'httpputformcontentfilter' to: [/*]2018-06-24 1951.163  info 11207 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'requestcontextfilter' to: [/*]2018-06-24 1951.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 1948 cst 2018]; root of context hierarchy2018-06-24 1951.724  info 11207 --- [           main] s.w.s.m.m.a.requestmappinghandlermapping : mapped {[/get],methods=[get]} onto public com.sunny.entity.responsemsg com.sunny.controller.blockcontroller.getresult()2018-06-24 1951.730  info 11207 --- [           main] s.w.s.m.m.a.requestmappinghandlermapping : mapped {[/error]} onto public org.springframework.http.responseentity org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)2018-06-24 1951.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 1951.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 1951.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 1951.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 1952.126  info 11207 --- [           main] o.s.j.e.a.annotationmbeanexporter        : registering beans for jmx exposure on startup2018-06-24 1952.205  info 11207 --- [           main] s.b.c.e.t.tomcatembeddedservletcontainer : tomcat started on port(s): 8080 (http)2018-06-24 1952.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}  
然后我们来看看控制台的日志:
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 result = taskservice.getresult();”的时候是阻塞了大约30秒钟,随后才执行它后面的打印语句“log.info(“接收任务线程完成并退出”);”。
callable异步调用
涉及到较长时间的请求处理的话,比较好的方式是用异步调用,比如利用callable返回结果。异步主要表现在,接收请求的servlet可以不用持续等待结果产生,而可以被释放去处理其他事情。当然,在调用者来看的话,其实还是表现在持续等待30秒。这有利于服务端提供更大的并发处理量。
这里我们新建一个callabledemo模块,在这个模块中,我们一样只包含一个taskcontroller,另外也是需要加入base模块的依赖。只不过这里我们的返回值不是responsemsg类型了,而是一个callable类型。
@restcontrollerpublic class taskcontroller {    private static final logger log = loggerfactory.getlogger(taskcontroller.class);    @autowired    private taskservice taskservice;    @requestmapping(value = /get,method = requestmethod.get)    public callable getresult(){        log.info(接收请求,开始处理...);        callable 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: default2018-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 hierarchy2018-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 1916.762  info 11226 --- [           main] o.apache.catalina.core.standardservice   : starting service [tomcat]2018-06-24 1916.764  info 11226 --- [           main] org.apache.catalina.core.standardengine  : starting servlet engine: apache tomcat/8.5.232018-06-24 1916.918  info 11226 --- [ost-startstop-1] o.a.c.c.c.[tomcat].[localhost].[/]       : initializing spring embedded webapplicationcontext2018-06-24 1916.919  info 11226 --- [ost-startstop-1] o.s.web.context.contextloader            : root webapplicationcontext: initialization completed in 2126 ms2018-06-24 1917.144  info 11226 --- [ost-startstop-1] o.s.b.w.servlet.servletregistrationbean  : mapping servlet: 'dispatcherservlet' to [/]2018-06-24 1917.149  info 11226 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'characterencodingfilter' to: [/*]2018-06-24 1917.150  info 11226 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'hiddenhttpmethodfilter' to: [/*]2018-06-24 1917.150  info 11226 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'httpputformcontentfilter' to: [/*]2018-06-24 1917.150  info 11226 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'requestcontextfilter' to: [/*]2018-06-24 1917.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 1914 cst 2018]; root of context hierarchy2018-06-24 1917.726  info 11226 --- [           main] s.w.s.m.m.a.requestmappinghandlermapping : mapped {[/get],methods=[get]} onto public java.util.concurrent.callable com.sunny.controller.taskcontroller.getresult()2018-06-24 1917.731  info 11226 --- [           main] s.w.s.m.m.a.requestmappinghandlermapping : mapped {[/error]} onto public org.springframework.http.responseentity org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)2018-06-24 1917.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 1917.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 1917.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 1917.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 1918.084  info 11226 --- [           main] o.s.j.e.a.annotationmbeanexporter        : registering beans for jmx exposure on startup2018-06-24 1918.176  info 11226 --- [           main] s.b.c.e.t.tomcatembeddedservletcontainer : tomcat started on port(s): 8081 (http)2018-06-24 1918.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}  
和阻塞调用的结果一样——当然一样啦,都是同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使用场景。
场景一:
创建一个持续在随机间隔时间后从任务队列中获取任务的线程
访问controller中的方法,创建一个deferredresult,设定超时时间和超时返回对象
设定deferredresult的超时回调方法和完成回调方法
将deferredresult放入任务中,并将任务放入任务队列
步骤1中的线程获取到任务队列中的任务,并产生一个随机结果返回
场景其实非常简单,接下来我们来看看具体的实现。首先,我们还是来看任务实体类是怎么样的。
/** * 任务实体类 */@data@noargsconstructor@allargsconstructorpublic class task {    private int taskid;    private deferredresult taskresult;    @override    public string tostring() {        return task{ +                taskid= + taskid +                , taskresult + {responsemsg= + taskresult.getresult() + } +                '}';    }}  
看起来非常简单,成员变量又taskid和taskresult,前者是int类型,后者为我们的deferredresult类型,它的泛型类型为responsemsg,注意这里用到responsemsg,所以也需要导入base模块的依赖。
另外注解之前已经说明了,不过这里再提一句,@data注解也包含了tostring的重写,但是这里为了知道具体的responsemsg的内容,sunny特意手动重写。
看完task类型,我们再来看看任务队列。
@componentpublic class taskqueue {    private static final logger log = loggerfactory.getlogger(taskqueue.class);    private static final int queue_length = 10;    private blockingqueue queue = new linkedblockingdeque(queue_length);    private int taskid = 0;    /**     * 加入任务     * @param deferredresult     */    public void put(deferredresult 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来高效处理并发,因为它属于无界非阻塞队列,使用过程中需要考虑可能造成的oom问题。sunny这里选择阻塞队列linkedblockingdeque,它底层使用加锁进行了同步;但是这里使用了taskqueue进行封装,处理过程中有一些额外操作,调用时需要加锁以防发生某些意料之外的问题。
然后我们来看步骤1中的,启动一个持续从任务队列中获取任务的线程的具体实现。
@componentpublic 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 responsemsg = new responsemsg(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 这里,我们注入了taskqueue,成员变量比较简单并且有注释,不再说明,主要来看方法。先看一下最后一个方法getrandomstr,很显然,这是一个获得长度为len的随机串的方法,访问限定为private,为类中其他方法服务的。然后我们看init方法,它执行的其实就是开启了一个线程并且执行execute方法,注意一下它上面的@postcontruct注解,这个注解就是在这个bean初始化的时候就执行这个方法。
所以我们需要关注的实际逻辑在execute方法中。可以看到,在execute方法中,用了一个while(true)来保证线程持续运行。因为是并发环境下,考虑对taskqueue加锁,从中取出任务;如果任务不为空,获取用getrandomstr生成一个随机结果并用setresult方法进行返回。
最后可以看到,利用random生成来一个[0,10)的随机数,并让线程sleep相应的秒数。这里注意一下,需要设定一个时间间隔,否则,先线程持续跑会出现cpu负载过高的情况。
接下来我们就看看controller是如何处理的。
@restcontrollerpublic class taskcontroller {    private static final logger log = loggerfactory.getlogger(taskcontroller.class);    //超时结果    private static final responsemsg 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 getresult() {        log.info(接收请求,开始处理...);        //建立deferredresult对象,设置超时时间,以及超时返回超时结果        deferredresult 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。这里我们首先创建了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: default2018-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 hierarchy2018-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 2130.980  info 11322 --- [           main] o.apache.catalina.core.standardservice   : starting service [tomcat]2018-06-24 2130.981  info 11322 --- [           main] org.apache.catalina.core.standardengine  : starting servlet engine: apache tomcat/8.5.232018-06-24 2131.062  info 11322 --- [ost-startstop-1] o.a.c.c.c.[tomcat].[localhost].[/]       : initializing spring embedded webapplicationcontext2018-06-24 2131.063  info 11322 --- [ost-startstop-1] o.s.web.context.contextloader            : root webapplicationcontext: initialization completed in 2066 ms2018-06-24 2131.207  info 11322 --- [ost-startstop-1] o.s.b.w.servlet.servletregistrationbean  : mapping servlet: 'dispatcherservlet' to [/]2018-06-24 2131.212  info 11322 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'characterencodingfilter' to: [/*]2018-06-24 2131.213  info 11322 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'hiddenhttpmethodfilter' to: [/*]2018-06-24 2131.213  info 11322 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'httpputformcontentfilter' to: [/*]2018-06-24 2131.213  info 11322 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'requestcontextfilter' to: [/*]2018-06-24 2131.247  info 11322 --- [           main] com.sunny.bean.taskexecute               : 开始持续处理任务2018-06-24 2131.249  info 11322 --- [       thread-8] com.sunny.bean.taskqueue                 : 获得任务:null2018-06-24 2131.250  info 11322 --- [       thread-8] com.sunny.bean.taskexecute               : 处理间隔:6秒2018-06-24 2131.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 2128 cst 2018]; root of context hierarchy2018-06-24 2131.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.controller.taskcontroller.getresult()2018-06-24 2131.576  info 11322 --- [           main] s.w.s.m.m.a.requestmappinghandlermapping : mapped {[/error]} onto public org.springframework.http.responseentity org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)2018-06-24 2131.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 2131.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 2131.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 2131.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 2131.811  info 11322 --- [           main] o.s.j.e.a.annotationmbeanexporter        : registering beans for jmx exposure on startup2018-06-24 2131.892  info 11322 --- [           main] s.b.c.e.t.tomcatembeddedservletcontainer : tomcat started on port(s): 8082 (http)2018-06-24 2131.897  info 11322 --- [           main] com.sunny.deferredresultapplication      : started deferredresultapplication in 3.683 seconds (jvm running for 4.873)2018-06-24 2137.254  info 11322 --- [       thread-8] com.sunny.bean.taskqueue                 : 获得任务:null2018-06-24 2137.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                 : 获得任务:null2018-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}  
可以看到我们的随机串是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为:12018-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获取请求并进行处理结果返回。
场景二
用户发送请求到taskcontroller的getresult方法,该方法接收到请求,创建一个deferredresult,设定超时时间和超时返回对象
设定deferredresult的超时回调方法和完成回调方法,超时和完成都会将本次请求产生的deferredresult从集合中remove
将deferredresult放入集合中
另有一个taskexecutecontroller,访问其中一个方法,可取出集合中的等待返回的deferredresult对象,并将传入的参数设定为结果
首先我们来看看deferredresult的集合类:
@component@datapublic class taskset {    private set set = new hashset();}  
非常简单,只包含了一个hashset的成员变量。这里可以考虑用concurrenthashmap来实现高效并发,sunny这里简单实用hashset,配合加锁实现并发处理。
然后我们看看发起调用的controller代码:
@restcontrollerpublic class taskcontroller {    private logger log = loggerfactory.getlogger(taskcontroller.class);    //超时结果    private static final responsemsg 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 getresult(){        log.info(接收请求,开始处理...);        //建立deferredresult对象,设置超时时间,以及超时返回超时结果        deferredresult 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:
@restcontrollerpublic 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 res = new responsemsg(0,success,result);        log.info(结果处理开始,得到输入结果为:{},res);        set set = taskset.getset();        synchronized (set){            set.foreach((deferredresult)->{deferredresult.setresult(res);});        }        return successfully set result:  + result;    }}  
看起来非常简单,只是做了两个操作,接收得到的参数并利用参数生成一个responsemsg对象,随后将集合中的所有deferredresult都设定结果为根据参数生成的responsemsg对象。最后返回一个提示:成功设置结果…
好了,话不多说,我们来启动测试验证一下。我们说一下验证的过程,我们同时打开两个请求,然后再设定一个结果,最后两个请求都会得到这个结果。当然同时多个或者一个请求也是一样。这里有一个地方需要注意一下:
浏览器可能会对相同的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: default2018-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 hierarchy2018-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 0045.658  info 12688 --- [           main] o.apache.catalina.core.standardservice   : starting service [tomcat]2018-06-25 0045.659  info 12688 --- [           main] org.apache.catalina.core.standardengine  : starting servlet engine: apache tomcat/8.5.232018-06-25 0045.722  info 12688 --- [ost-startstop-1] o.a.c.c.c.[tomcat].[localhost].[/]       : initializing spring embedded webapplicationcontext2018-06-25 0045.723  info 12688 --- [ost-startstop-1] o.s.web.context.contextloader            : root webapplicationcontext: initialization completed in 1241 ms2018-06-25 0045.817  info 12688 --- [ost-startstop-1] o.s.b.w.servlet.servletregistrationbean  : mapping servlet: 'dispatcherservlet' to [/]2018-06-25 0045.821  info 12688 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'characterencodingfilter' to: [/*]2018-06-25 0045.821  info 12688 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'hiddenhttpmethodfilter' to: [/*]2018-06-25 0045.821  info 12688 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'httpputformcontentfilter' to: [/*]2018-06-25 0045.821  info 12688 --- [ost-startstop-1] o.s.b.w.servlet.filterregistrationbean   : mapping filter: 'requestcontextfilter' to: [/*]2018-06-25 0046.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 0044 cst 2018]; root of context hierarchy2018-06-25 0046.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.controller.taskcontroller.getresult()2018-06-25 0046.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 0046.202  info 12688 --- [           main] s.w.s.m.m.a.requestmappinghandlermapping : mapped {[/error]} onto public org.springframework.http.responseentity org.springframework.boot.autoconfigure.web.basicerrorcontroller.error(javax.servlet.http.httpservletrequest)2018-06-25 0046.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 0046.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 0046.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 0046.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 0046.362  info 12688 --- [           main] o.s.j.e.a.annotationmbeanexporter        : registering beans for jmx exposure on startup2018-06-25 0046.467  info 12688 --- [           main] s.b.c.e.t.tomcatembeddedservletcontainer : tomcat started on port(s): 8083 (http)2018-06-25 0046.472  info 12688 --- [           main] com.sunny.deferredresultapplication      : started deferredresultapplication in 2.675 seconds (jvm running for 3.623)  
完美启动,接下来sunny在火狐中发起一个请求
可以看到正在等待请求结果。随后我们在谷歌浏览器中发起请求
两个请求同时处于等待状态,这时候我们看一下控制台信息:
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      : 加入任务集合,集合大小为:12018-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      : 加入任务集合,集合大小为:22018-06-25 00:22:37.332  info 12688 --- [nio-8083-exec-7] com.sunny.controller.taskcontroller      : 接收任务线程完成并退出  
可以看到两个请求都已经接收到了,并且加入了队列。这时候,我们再发送一个设置结果的请求。
随后我们查看两个调用请求的页面,发现页面已经不在等待状态中了,都已经得到了结果。
另外,再给大家展示一下超时的结果,即我们发起调用请求,但是不发起设置结果的请求,等待时间结束。
查看控制台信息:
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      : 加入任务集合,集合大小为:12018-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      : 调用超时,移除任务,此时队列长度为12018-06-25 00:27:16.018  info 12688 --- [nio-8083-exec-5] com.sunny.controller.taskcontroller      : 调用完成,移除任务,此时队列长度为0  
后记
想要完整代码的童鞋,点这里:
https://gitee.com/sunnymore/asynchronous_task

利亚德:拟回购不超过1573.98万股公司股份 价格不超过9.53元/股
政策清障添助力 新能源汽车获多重利好
一种非接触式单相费控智能电表的设计
基于AT89S52单片机的太阳能环境参数测试仪的系统设计
某运营商高层领导敲警钟,宽带发展面临较大压力
DeferredResult异步请求处理 提高系统吞吐量的一把利器
变量及赋值是什么
选智能马桶还要看质量与技术设计
吴新宙离职之后:小鹏汽车自动驾驶的历史与未来挑战
东大索尼共同研发出AI系统Derma 传感器贴合在喉咙把口形转为声音
微流控技术为高通量药物发现提供与人体生理学相关的细胞模型
区块链应用平台Crown介绍
为什么GPU获得了如此多的缓存?
罗森伯格HPK高压连接器为广大客户创造更大价值
双频超声波清洗机怎么样?
机场航站楼消防应急照明和疏散指示系统设计要点剖析
基于SCSI协议处理器FAS466实现图像数据存储系统的设计
机器视觉知识--手写字体是怎么识别的?
乾照光电宣布全资子公司乾照半导体收到研发投入补助款3000万元
log2在verilog中到底有什么用