使用 Servlet3.1 WriteListener进行异步响应输出以及踩过的坑

在标准的spring mvc项目中,当对象数据已经产生时,不管是输出json,还是view,最终均是将相应的数据直接写入response的outputStream中。这里的输出是同步式调用的,即意味着只有当整个数据完整地写到输出流中时,整个业务线程才会被认为执行结束。

这就意味着在传统的基于请求的慢速攻击之外,还有一种基于响应的慢速攻击,其思路即是客户端强制让整个接收变慢,而服务端因为缓存区不足以存储完整的响应数据,而被动地阻塞调用端,进而阻塞业务线程。

在Servlet 3.0版本中,提出了业务中的异步处理,即 通过 HttpServletRequest#startAsync 来开启异步处理。等业务处理完成之后,才通过 AsyncContext#complete 来结束处理。在中间,可以仍然通过 HttpServletResponse#getOutputStream 来输出数据. 这里的异步处理仅仅是将容器的处理线程解放出来,即当业务处理需要长时间或者需要故意挂起请求(如LongPull)时,才有相应的意义。在最终输出数据时,这里仍然是阻塞的,只不过是阻塞业务线程(而非容器线程).造成的后果即是缓慢地IO操作造成线程阻塞。

随着 NIO 的出现,所有Web容器均使用了Nio来处理请求,即当整个请求已经完整读取完毕之后,才转交由具体的业务线程来处理,即在这里的io读取均为非阻塞读取,由很少数的线程就可以处理上百(千)个请求的网络请求. 在 Servlet 3.1 版本中, 规范中给出了 异步响应的概念,即可以通过响应式来达到异步非阻塞输出数据的效果. 从调用端来看,调用write之后即刻返回,并不会阻塞业务线程。后续未刷新到缓冲区的数据将在后续通过标准Nio流程由web容器的IO线程负责处理.

一个参考使用例子如下所示(参考于 https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/HTML5andServlet31/HTML5andServlet%203.1.html):

//开启异步
val context = request.startAsync();
val response = context.getResponse();
val servletOut = response.getOutputStream();

//定义响应监听器,以可写时输出相应的数据
val writeListener = new WriteListenerImpl(context, servletOut, actionQueue);
servletOut.setWriteListener(writeListener);

//一个参考的监听器实现
public class WriteListenerImpl implements WriteListener {
    private ServletOutputStream output = null;
    private Queue queue = null;
    private AsyncContext context = null;

    private boolean flag = false;

    @Override
    public void onWritePossible() throws IOException {
        if (flag) {
            return;
        }

        //这里仅当output.isReady() 时才会调用write方法,否则会报 IllegalStateException
        while (queue.peek() != null && output.isReady()) {
            val action = queue.poll();
            doWrite(action);
        }
    }

    private void doWrite(Action action) throws IOException {
        //已完成标记位
        if (action == Action.COMPLETE) {
            flag = true;
        }

        action.run(context, servletOut);
    }
}
继续阅读“使用 Servlet3.1 WriteListener进行异步响应输出以及踩过的坑”

Jetty中如何实现servlet的异步Request调用

在servlet3.1规范当中,已经支持在servlet中通过调用request.startAsync来开启1个异步调用,然后在相应的业务线程里面进行一些业务操作,再通过asyncContext.complete即完成业务的整个操作。一个参考的demo如下所示:

    val context = req.startAsync();
    //重新设置业务超时时间
    context.setTimeout(40_000);

    Runnable runnable = () -> {
        try{
            //执行你的业务操作

            //输出数据
            context.getResponse().getWriter().println(totalMoney);

            //完成业务
            context.complete();
        } catch(Exception e) {
            e.printStackTrace();
        }
    };
    
    new Thread(runnable).start();

在上面的参考中,原来的servlet在调用完 thread.start之后,相应的逻辑即完成。相应的容器线程则已经还给线程池,此线程即可以接收其它客户端的请求并处理了. 异步servlet的目的即在于将接收请求的io线程与实际的业务执行相分开,避免过慢的业务阻塞了整个容器,而不能再接收更多的请求了.甚至可以在后端的业务池中定义一个队列,将要进行执行的业务逻辑放入队列里面慢慢执行。

由于异步Servlet的目的在于将web容器的io线程与业务线程分离,那么关键的部分即在于当servlet方法执行完之后,当context.complete时,如何重新触发相应io流的操作。本文尝试以jetty为参考,从源码角度查看其实现的原理和机制。

本文参考的jetty版本为 9.4.11.v20180605

继续阅读“Jetty中如何实现servlet的异步Request调用”

JavaScript的单线程性质以及同步ajax提交

本文转自:http://www.phpweblog.net/rainman/archive/2009/01/05/6267.html

How JavaScript Timers Work

从基础的层面来讲,理解JavaScript的定时器是如何工作的是非常重要的。计时器的执行常常和我们的直观想象不同,那是因为JavaScript引擎是单线程的。我们先来认识一下下面三个函数是如何控制计时器的。

  • var id = setTimeout(fn, delay); – 初始化一个计时器,然后在指定的时间间隔后执行。该函数返回一个唯一的标志ID(Number类型),我们可以使用它来取消计时器。
  • var id = setInterval(fn, delay); – 和setTimeout有些类似,但它是连续调用一个函数(时间间隔是delay参数)直到它被取消。
  • clearInterval(id);, clearTimeout(id); – 使用计时器ID(setTimeout 和 setInterval的返回值)来取消计时器回调的发生

为了理解计时器的内在执行原理,有一个重要的概念需要加以探讨:计时器的延迟(delay)是无法得到保障的。由于所有JavaScript代码是在一个线程里执行的,所有异步事件(例如,鼠标点击和计时器)只有拥有执行机会时才会执行。用一个很好的图表加以说明:

在这个图表中有许多信息需要理解,如果完全理解了它们,你会对JavaScript引擎如何实现异步事件有一个很好的认识。

继续阅读“JavaScript的单线程性质以及同步ajax提交”

基于Spring打造简单高效通用的异步任务处理系统(转)

本文之前,今天发现使用spring-batch也能够达到相同的效果,不过相比spring-batch,本文使用的数据表更少,且相应的逻辑更简洁,特转如下。
本文转自:http://blog.csdn.net/sfdev/article/details/4056114 原文作者:sfdev

背景

随着应用系统功能的不断新增,而某些功能的实现对实时性要求并不是那么高,但是逻辑却很复杂、执行比较耗时,比如涉及外部系统调用、多数据源等等;此时,我们就希望可以让这些复杂的业务逻辑放在后台执行,而前台与用户的交互可以不用等待,从而提高用户体验;

另外,从系统架构这个层面来说,我们也希望按照不同功能来拆分,以保持各个系统之间的低耦合,当一个系统出现问题时不会影响到其他系统,并且对于独立的各个系统,我们可以专门进行性能优化、监控等;所以我们需要通用、高效的异步任务处理系统;

设计目标

打造轻量级、简单、高效、通用、高扩展性、高可靠性的异步任务处理系统!

系统设计

要实现类似的异步处理系统,相信大家首先想到的就是JMS,Alibaba里面也有基于JMS的异步处理系统,而且该系统在网店系统中应用非常广泛;但由于目前我们阿里软件采用了不同的技术框架,所以不能直接拿来使用;况且,该系统为了实现异步任务系统的并发,采取了JMS与MDB结合的策略,所以系统就依赖于EJB了,这样系统就变得笨重了,由此系统部署的应用服务器必须要支持EJB,一些轻量级的不支持EJB规范的应用服务器就没法部署了;

考虑到如上的系统设计目标,我们的设计思路为:任务DB持久化 + Spring封装Job调度、线程池

继续阅读“基于Spring打造简单高效通用的异步任务处理系统(转)”