Tomcat长轮训原理解析

312 阅读2分钟

长轮询

  1. 长轮询基于HTTP,用于数据更新的实时通知
  2. 广泛用于中间件配置实时同步通知如Apollo、Nacos

什么是长轮询

客户端向服务器请求,服务器接到请求后hold住连接,并等待数据返回(Java可以用AsyncContext来实现,spring中可以用DeferredResult来hold住请求), 直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求

缺点:客户端需要一直保持着这个链接,相当于这个资源一直在占用着。

Spring实现异步请求

  1. 客户端发起请求到Tomcat服务器
  2. Tomcat服务器开启异步线程等待通知,然后hold住线程
  3. 等到有结果或超时,Tomcat服务器就会返回结果给客户端 长轮询图.png

上面大概知道实现长连接的基本原理。但tomcat是怎么实现的?见下面

Tomcat实现长连接

实现步骤(通过DeferredResult实现)

  1. ReturnValueHandler遇到返回结果为DeferredResult时会开启异步处理,标记为异步请求
  2. Tomcat会把异步请求放入WaitingProcessors的Set集合,并让出Tomcat当前执行线程
  3. 当调用setResult、等待超时会触发dispatch事件,变更异步请求的状态
  4. Adapter重新链接Container,finish request和response,往通道中写入结果响应客户端

测试DEMO

分为订阅配置以及发布配置两个接口。首先订阅配置进去等待结果返回或超时终止,如果这时候发布配置就会触发tomcat对应异步线程执行返回数据

@RestController
@Slf4j
public class DeferredResultDemoController {
    /** 写一个简单的demo,多个的时候可以使用map来做映射 **/
    private static final Map<String, DeferredResult<ResponseEntity<DataConfig>>> deferredResult = new ConcurrentHashMap<>();
    // 配置监听接入点
    @ResponseBody
    @RequestMapping("/longpoll/deferred/listener")
    public DeferredResult<ResponseEntity<DataConfig>> addListener2(String dataId) {
        long start = System.currentTimeMillis();
        log.info("listener start");
        // 设置90s超时
        DeferredResult<ResponseEntity<DataConfig>> value =
                deferredResult.getOrDefault(dataId, new DeferredResult<>(1000 * 90L));
        // 绑定
        deferredResult.putIfAbsent(dataId, value);
        ForkJoinPool.commonPool().submit(() -> {
            log.info("开始执行进程");
            value.onCompletion(() -> log.info("执行完成"));
            value.onTimeout(() -> value.setErrorResult(ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
                    .body("超时出现异常: " + (System.currentTimeMillis() - start))));
            log.info("开始执行完成");
        });
        log.info("listener end cost={} ms", (System.currentTimeMillis() - start));
        return value;
    }
    // 配置发布接入点
    @RequestMapping("/longpoll/deferred/publishConfig")
    @SneakyThrows
    @ResponseBody
    public String publishConfig2(String dataId, String configInfo) {
        log.info("publish2 configInfo dataId: [{}], configInfo: {}", dataId, configInfo);
        DeferredResult<ResponseEntity<DataConfig>> value = deferredResult.get(dataId);
        if (value == null) throw new Exception("config not exist");
        ResponseEntity<DataConfig> responseEntity = new ResponseEntity<>(new DataConfig(dataId, configInfo), HttpStatus.OK);
        value.setResult(responseEntity);
        // 解除绑定
        deferredResult.remove(dataId);
        return "success";
    }
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DataConfig {
    private String dataId;
    private String configInfo;
}

Tomcat实现异步原理

根据上面例子解析大概流程如下
1、returnValueHandler遇到返回结果为DeferredResult时会开启异步处理,标记为异步请求
2、Tomcat会把异步请求放入WaitingProcessors的Set集合,并让出Tomcat当前执行线程
3、当调用setResult、等待超时会触发dispatch事件,变更异步请求的状态
4、Adapter重新链接Container,finish request和response,往通道中写入结果响应客户端
备注:Tomcat超时检测只能精确到秒级别

Tomcat处理流程

1652529369.png

请求挂起

tomcat请求挂起.png

超时及手动设置结果

tomcat超时及手动设置结果.png

参考:lzg.eoeoi.com