客服系统服务中使用Servlet3.0异步长轮训,服务压力大时,导致消息错乱。
描述 对Servlet请求应答对象的生命周期理解不够深入,IM服务在服务压力大,并且Nginx断开请求回收资源后,依然将用户消息进行下发,最后导致消息下发到其他请求中。
原因分析 错误代码伪代码 服务使用异步的Servlet方法进行长轮训
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @WebServlet(asyncSupported = true) public class CometServlet extends HttpServlet { @Override protected void doPost (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { AsyncContext context = request.startAsync(); context.setTimeout(TIMEOUT); context.start(() -> { Thread.sleep(27 * 1000 ); response.getWriter().write(...); context.complete(); }); } }
问题描述
如果发生超时或者Nginx因为某些原因频繁断开与Tomcat之间的连接,Request以及Response对象会被Tomcat发现并且执行清理。
如果清理只是销毁对象的话也还不会导致问题,但查看源码发现,Tomcat是把对象回收,交给下一个请求使用。当Tomcat回收Repsonse对象,交给下一个请求使用后,回调函数依然会继续执行,Response虽然是同一个对象,但已经是其他请求正在使用的了,继续往里面下发消息则导致了整个事件的发生。
问题分析 而在Tomcat提供的AsyncContext里面会感知到连接异常,并且提供清理操作,以下是AsyncContextImpl源码节选。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class AsyncContextImpl implements AsyncContext , AsyncContextCallback { private volatile ServletRequest servletRequest = null ; private volatile ServletResponse servletResponse = null ; public void setStarted (Context context, ServletRequest request, ServletResponse response, boolean originalRequestResponse) { ... this .servletRequest = request; this .servletResponse = response; ... } public void recycle () { if (log.isDebugEnabled()) { logDebug("recycle " ); } context = null ; dispatch = null ; event = null ; hasOriginalRequestAndResponse = true ; instanceManager = null ; listeners.clear(); request = null ; clearServletRequestResponse(); timeout = -1 ; } private void clearServletRequestResponse () { servletRequest = null ; servletResponse = null ; } private void check () { if (request == null ) { throw new IllegalStateException (sm.getString("asyncContextImpl.requestEnded" )); } } @Override public ServletRequest getRequest () { check(); if (servletRequest == null ) { throw new IllegalStateException (sm.getString("asyncContextImpl.request.ise" )); } return servletRequest; } @Override public ServletResponse getResponse () { check(); if (servletResponse == null ) { throw new IllegalStateException (sm.getString("asyncContextImpl.response.ise" )); } return servletResponse; } }
结果及处理 以上源码分析可以看到,Tomcat的AsyncContext针对连接断开、超时等情况是有做特殊保护处理的,而IM服务所用的方式并没有用上Tomcat的保护,直接将应答对象写入了错误的应答。
正确的使用方式很简单:
**只需要把response.getWriter().write(…); **
修改为context.getResponse().getWriter().write(…)即可,就是这么一行代码,导致了整个事件的发生。
另外,使用Spring提供的DeferredResult已完全封装了异步请求,可避免此问题。