长轮询
- 长轮询基于HTTP,用于数据更新的实时通知
- 广泛用于中间件配置实时同步通知如Apollo、Nacos
什么是长轮询
客户端向服务器请求,服务器接到请求后hold住连接,并等待数据返回(Java可以用AsyncContext来实现,spring中可以用DeferredResult来hold住请求), 直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
缺点:客户端需要一直保持着这个链接,相当于这个资源一直在占用着。
Spring实现异步请求
- 客户端发起请求到Tomcat服务器
- Tomcat服务器开启异步线程等待通知,然后hold住线程
- 等到有结果或超时,Tomcat服务器就会返回结果给客户端
上面大概知道实现长连接的基本原理。但tomcat是怎么实现的?见下面
Tomcat实现长连接
实现步骤(通过DeferredResult实现)
- ReturnValueHandler遇到返回结果为DeferredResult时会开启异步处理,标记为异步请求
- Tomcat会把异步请求放入WaitingProcessors的Set集合,并让出Tomcat当前执行线程
- 当调用setResult、等待超时会触发dispatch事件,变更异步请求的状态
- Adapter重新链接Container,finish request和response,往通道中写入结果响应客户端
测试DEMO
分为订阅配置以及发布配置两个接口。首先订阅配置进去等待结果返回或超时终止,如果这时候发布配置就会触发tomcat对应异步线程执行返回数据
@RestController
@Slf4j
public class DeferredResultDemoController {
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");
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处理流程
请求挂起
超时及手动设置结果
参考:http://lzg.eoeoi.com
|