1. 简介
长轮询是与服务器保持即时通信的最简单方式,它不依赖双向通信协议,例如 WebSocket,只需要支持http协议即可 ,也不依赖于浏览器版本等外部条件的兼容性,它很容易实现,也无需引入其他依赖,在很多场景下可以作为即时通信的最简单实现方案和兜底兼容方案。
2. 长轮询(Long polling)和常规轮询的区别
2.1 常规轮询
从服务器获取新信息的最简单的方式是定期轮询。也就是说,定期向服务器发出请求:"你好,我在这儿,你有关于我的任何消息吗?"例如,每 10 秒一次。
作为响应,服务器立即向客户端发送自上一次轮询到目前为止的消息包,即便当前可能并没有新的消息。
这可行,但是也有缺点:
- 消息传递的延迟最多为 10 秒(两个请求之间)。
- 即使没有消息,客户端为了维持消息一定的即时性,服务器也会每隔 10 秒被请求一次,就性能而言,这是一个很大的负担。
因此,如果我们讨论的是一个很小负载的服务,那么这种方式可能可行,但总的来说,不足之处也很明显。
示意图:
2.2 长轮询
相比于常规轮询,长轮询的无效请求可以大大减少,另外也具有类似实时推送的特性,消息具有实时性。
其流程为:
- 请求发送到服务器。
- 服务器在有新数据之前不会响应请求。
- 当新数据出现、或超过预设等待时间时,服务器将对其请求作出响应。
- 浏览器立即发出一个新的请求。
此时轮询的主动权虽然理论上依然在浏览器,但由于浏览器下一次请求的时机实际上取决于服务器上一次响应的动作,所以服务器端可以控制客户端轮询的频率,具体的方式就是服务器端设置长轮询超时响应时间。
这个时间如果设置为20秒,含义是如果浏览器发起请求20秒内某一个时间点有了新消息,则立即响应浏览器,如果20秒内没有新消息,则返回一个超时状态的响应给浏览器,浏览器开启下一次长轮询。
示意图:
3. Servlet的同步与异步
长轮询挂起请求,等待新数据更新后再响应的处理方式虽然能减少浏览器的请求次数,并带来即时性,但是如果使用同步处理请求的方式,挂起请求则代表了线程的阻塞,有多少长轮询请求未响应就代表要阻塞多少个Servlet线程,这显然是不合理的,所以长轮询必须使用异步处理的方式,而Servlet 3.0开始已经支持了异步处理。
3.1 同步处理请求:
从上图可以看出:请求到达后,从主线程池获取一个线程,处理业务,响应请求,然后将线程还回线程池,整个过程都是由同一个主线程在执行。
这里存在一个问题,通常 Servlet容器的主线程数量是有限的,若执行业务的比较耗时,大量请求过来之后,主线程被耗光,新来的请求就会处于等待状态,而长轮询的长时间等待动作,则会剧烈放大这个问题,主线程很容易就会处于全部阻塞的状态,无法处理新的请求。
而 servlet3.0 中对这个过程做了改进,主线程可以将请求转交给其他线程去处理,比如开发者可以自定义一个线程,然后在自定义的线程中处理请求。
3.2 异步处理请求:
如上图,在主线程中开启异步处理,主线程将请求交给其他线程去处理,主线程就结束了,被放回了主线程池,由其他线程继续处理请求,可以避免主线程池线程耗尽,起到容器线池程和工作线程池隔离的效果。
4. 异步处理的长轮询
仅仅是线程池隔离还是不够的,因为工作线程池依然会因为等待阻塞而面临耗尽的问题,解决方案是使用单独的守护线程或者监听器监听响应事件队列。
例如微信服务器回调消息就可以看做是一个响应事件,这个事件被包装成对象放入队列中,守护线程通过阻塞的方式获取到事件对象后调用自定义线程池中的工作线程,工作线程从请求池中取出并删除异步请求域对象AsyncContext,然后进行数据准备和任务处理,完成对前端的响应。
需要注意的是,每一个长轮询请求都会对应一个超时任务,这个超时任务会提交给ScheduledThreadPoolExecutor定时任务线程池,当超时时间到了后,会去请求池查找对应的请求是否还存在(如已响应则会被删除),如果还存在则会响应前端长轮询超时。
长轮询超时不是通常意义上的请求超时,而是在指定时间范围内没有新的数据需要返回,这个状态值需要和前端协调好,属于是正常的响应状态。
如下图所示:
当然,实际使用中会出现,新数据出现了但是请求对象已经超时响应过,但新的请求并没有发送到服务端的情形,这种情况可以理解为"用户下线了",此时需要标记用户有未送达的信息,并对响应事件做好持久化工作。
当用户再次上线发起长轮询时,判断用户是否有未送达的消息,如有则将持久化信息从右侧塞入响应事件队列中,优先响应给用户旧信息。
5. 案例
5.1 案例描述
用户在网页使用微信扫码登陆,并且需要同时关注公众号,需要用到公众号接收事件推送,用户使用手机微信app扫码后微信服务器向后端服务器回调扫码事件,由于前端无法感知到用户手机扫码的行为,所以停留在扫码登陆页面时需要持续轮询后端服务器用户是否已扫码登录,造成服务器的较大压力,于是利用Servlet3.0的异步特性,实现长轮询的方式来通信。
前端扫码登录页面请求后端微信二维码,后端返回微信二维码给前端时附带随机生成的scene_id,前端显示二维码供用户手机微信扫码,前端持续轮询后端用户是否已扫码(上一次轮询未成功登陆为开启下一次轮询的条件),但需带上scene_id以区分扫码用户,此时后端不同步返回前端请求结果,而是以异步响应式的方式等待微信服务器回调后,或是超过指定时间例如30秒后再返回(可能用户打开登陆页面后停留但未成功扫码),大大减少了前端访问后端的次数。
具体方式是,request对象通过startAsync()方法获得异步域对象AsyncContext,此时请求被挂起,并释放Servlet线程。请求异步域对象AsyncContext放入ConcurrentHashMap实现的请求池中,以scene_id为key。
微信服务器回调服务端时会带上扫码事件(登陆为scan,关注为subscribe)以及scene_id、openid,将回调数据包装为响应事件放入响应事件队列中。
使用一个守护线程监听该队列,以阻塞的方式获取队列中的响应事件对象,并调用自定义线程池,根据scene_id从请求池中找到还未响应返回的AsyncContext对象,并从中获得response对象,根据JSON对象中的openId查询到用户信息,然后返回给前端。
如果30s超时后仍未接收到微信服务器的回调,则超时返回响应前端。
相比普通定时轮询,由于采用响应式设计,可以大幅降低前端请求频率和服务器负载,并在事件发生后第一时间通知前端,没有轮询周期空挡导致的延迟,相比websocket则可以并避免浏览器版本过低、网关对长连接的兼容性等问题,实现上也更加简单。
5.1 案例Demo
java
@RestController
@RequestMapping("/longPolling")
@Slf4j
public class LongPollingController {
//发送长轮询请求示例
@GetMapping("/poll/{requestId}")
public void poll(HttpServletRequest request, @PathVariable String requestId) {
//启动长轮询,设置
AsyncTaskUtil.longPolling(request ,requestId, 30);
}
//触发长轮询立即回复事件
@GetMapping("/call/{requestId}/{content}")
public void call(@PathVariable String requestId,@PathVariable String content) {
AsyncTaskUtil.addLongPollingRespEvent(requestId,() -> {
return R.ok("处理后结果:"+content);
});
}
//模拟tomcat容器线程阻塞
@GetMapping("/block/{resp}")
public String test(@PathVariable String resp) throws InterruptedException {
log.info("进入test controller---resp={},thread={}",resp,Thread.currentThread().getName());
Thread.currentThread().sleep(10000);
return resp;
}
}
java
package com.tsd.dlmPlatform_common.util;
import com.alibaba.fastjson.JSON;
import com.tsd.dlmPlatform_common.constant.ExceptionEnum;
import com.tsd.dlmPlatform_common.responseEntity.R;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.DependsOn;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.servlet.AsyncContext;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Supplier;
/**
*
* @author HeJun
* @date 2021-10-25
* @Version V1.0
* @Description 异步任务执行工具类
*/
@Slf4j
@Component
@DependsOn("contextUtil")
public class AsyncTaskUtil {
public static final int PROCESSOR_NUM = Runtime.getRuntime().availableProcessors();
private static final String EXECUTOR = "MY_EXECUTOR";
private static final int KEEP_ALIVE_SECONDS = 60;
public static final String SUCESS_RESP_STR = "执行成功";
private static final RejectedExecutionHandler MY_REJECTED_EXECUTION_HANDLER = ContextUtil
.getBean("myRejectedExecutionHandler", RejectedExecutionHandler.class);
private static volatile Map<String, ThreadPoolExecutor> THREAD_POOL_EXECUTOR_MAP = new ConcurrentHashMap<>();
private static volatile Map<String, AsyncContext> ASYNC_CONTEXT_MAP = new ConcurrentHashMap<>();
private static final LinkedBlockingDeque<LongPollingEvent<?>> LONG_POLLING_EVENT_QUEUE = new LinkedBlockingDeque<>();
private static final ScheduledThreadPoolExecutor TIME_OUT_CHECKER = new ScheduledThreadPoolExecutor(PROCESSOR_NUM,MY_REJECTED_EXECUTION_HANDLER);
/**
*
* @author HeJun
* @date 2021-10-25
* @Version V1.0
* @Description Bean销毁前关闭线程池
*/
@PreDestroy
private void destroyExecutor() {
log.info("关闭线程池。。。。。。。");
for (ThreadPoolExecutor executor : THREAD_POOL_EXECUTOR_MAP.values()) {
executor.shutdown();
}
TIME_OUT_CHECKER.shutdown();
}
@PostConstruct
private void init() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = ContextUtil.getBean(EXECUTOR, ThreadPoolTaskExecutor.class);
if(threadPoolTaskExecutor == null){
getOrAddThreadPoolExecutor(EXECUTOR,PROCESSOR_NUM,2*PROCESSOR_NUM,500,KEEP_ALIVE_SECONDS,true);
}else {
THREAD_POOL_EXECUTOR_MAP.put(EXECUTOR, threadPoolTaskExecutor.getThreadPoolExecutor());
}
}
/**
*
* @author HeJun
* @date 2022-05-06
* @Version V1.0
* @Description 返回单例定时器线程池
* @return
*/
public static ScheduledThreadPoolExecutor getTimeOutChecker(){
return TIME_OUT_CHECKER;
}
/**
*
* @author HeJun
* @date 2021-12-18
* @Version V1.0
* @Description 使用默认线程池执行有返回值的任务,不阻塞当前线程,可抛出异常,队列满则拒绝并抛出异常
* @param <T>
* @param supplier
* @return
*/
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) {
return supplyAsync(supplier, EXECUTOR, 0, 0, 0, 0,true);
}
/**
*
* @author HeJun
* @date 2021年4月28日
* @Version V1.0
* @Description 使用指定参数的线程池执行有返回值的任务,不阻塞当前线程,可抛出异常,用于隔离接口执行任务的线程池,避免单一接口拖垮整个系统,限制并发线程数提高执行效率
* @param <T>
* @param supplier
* @param poolName
* @param concurrency
* @param maxConcurrency
* @param queueSize
* @param isRejected 队列满是否拒绝任务,并抛出拒绝任务异常
* @return
*/
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier,String poolName,int concurrency,int maxConcurrency,int queueSize,int keepAliveSeconds,boolean isRejected) {
if(poolName == null) {
poolName = EXECUTOR;
}
ThreadPoolExecutor threadPoolExecutor = getOrAddThreadPoolExecutor(poolName, concurrency,maxConcurrency, queueSize,keepAliveSeconds,isRejected);
return CompletableFuture.supplyAsync(supplier, threadPoolExecutor);
}
/**
*
* @author HeJun
* @date 2021年9月24日
* @Version V1.0
* @Description 批量向线程池添加任务,并返回任务的异常信息
* @param poolName
* @param concurrency
* @param maxConcurrency
* @param queueSize
* @param isRejected
* @param sucessRespStr 任务正常运行没有抛出异常时返回的字符串
* @param task 返回任务集合的总任务
* @param timeoutSeconds 任务超时时间
* @return
*
*/
public static List<String> supplyAsyncAllWithExceptionMsgBatch(Supplier<List<Supplier<String>>> task,int timeoutSeconds,String poolName,int concurrency,int maxConcurrency,int queueSize,boolean isRejected,String sucessRespStr) {
if(sucessRespStr == null) {
throw new RuntimeException("正确回复模板不能为空");
}
List<CompletableFuture<String>> futureList = new ArrayList<>();
List<String> exMsgList = new ArrayList<>();
supplyAsync(task).thenAccept(supplierList -> {
for (Supplier<String> supplier : supplierList) {
CompletableFuture<String> supplyAsync = supplyAsyncWithExceptionMsg(supplier, poolName, concurrency, maxConcurrency, queueSize, isRejected);
futureList.add(supplyAsync);
}
}).join();
for (CompletableFuture<String> completableFuture : futureList) {
String result = null;
try {
result = completableFuture.get(timeoutSeconds,TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
result = "任务执行超时";
}
if(!sucessRespStr.equals(result)) {
exMsgList.add(result);
}
}
return exMsgList;
}
/**
*
* @author HeJun
* @date 2021年9月24日
* @Version V1.0
* @Description 批量向线程池添加任务,并返回任务的异常信息
* @param task
* @param sucessRespStr 任务正常运行没有抛出异常时返回的字符串
* @return
*
*/
public static List<String> supplyAsyncAllWithExceptionMsgBatch(Supplier<List<Supplier<String>>> task, String sucessRespStr) {
return supplyAsyncAllWithExceptionMsgBatch(task, 10, EXECUTOR, 0, 0, 0, true, sucessRespStr);
}
/**
*
* @author HeJun
* @date 2021年9月24日
* @Version V1.0
* @Description 提交任务到线程池,并返回异常信息
* @param supplier
* @param poolName
* @param concurrency
* @param maxConcurrency
* @param queueSize
* @param isRejected
* @return
*
*/
public static CompletableFuture<String> supplyAsyncWithExceptionMsg(Supplier<String> supplier,String poolName,int concurrency,int maxConcurrency,int queueSize,boolean isRejected){
return supplyAsync(supplier, poolName, concurrency, maxConcurrency, queueSize, KEEP_ALIVE_SECONDS,isRejected)
.exceptionally(t -> t.getCause() == null ? t.getMessage() : t.getCause().getMessage());
}
/**
*
* @author HeJun
* @date 2020-12-18
* @Version V1.0
* @Description 使用指定参数的线程池执行无返回值的任务,不阻塞当前线程,不可抛出异常
* @param runnable
* @return
*/
public static CompletableFuture<Void> runAsync(Runnable runnable,String poolName,int concurrency,int maxConcurrency,int queueSize,int keepAliveSeconds,boolean isReject) {
ThreadPoolExecutor threadPoolExecutor = getOrAddThreadPoolExecutor(poolName, concurrency,maxConcurrency, queueSize,keepAliveSeconds,isReject);
return CompletableFuture.runAsync(runnable, threadPoolExecutor);
}
/**
*
* @author HeJun
* @date 2020-12-18
* @Version V1.0
* @Description 使用默认线程池执行无返回值的任务,不阻塞当前线程,不可抛出异常
* @param runnable
* @return
*/
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return runAsync(runnable,EXECUTOR,0,0,0,0,true);
}
/**
*
* @author HeJun
* @date 2021年4月28日
* @Version V1.0
* @Description 获得指定名字的线程池,不存在则创建,线程安全
* @param poolName
* @param concurrency
* @param queueSize
* @param isRecject 队列已满是否拒绝
* @return
*/
public static ThreadPoolExecutor getOrAddThreadPoolExecutor(String poolName,int concurrency,int maxConcurrency,int queueSize,int keepAliveSecsonds,boolean isRecject) {
ThreadPoolExecutor threadPoolExecutor = THREAD_POOL_EXECUTOR_MAP.get(poolName);
if(threadPoolExecutor == null) {
synchronized (poolName.intern()){
// 双检锁
threadPoolExecutor = THREAD_POOL_EXECUTOR_MAP.get(poolName);
if(threadPoolExecutor == null){
// 生成一个指定参数表的线程池对象
RejectedExecutionHandler rejectedExecutionHandler = isRecject ? MY_REJECTED_EXECUTION_HANDLER : new ThreadPoolExecutor.CallerRunsPolicy();
keepAliveSecsonds = keepAliveSecsonds == 0 ? KEEP_ALIVE_SECONDS : keepAliveSecsonds;
threadPoolExecutor = new ThreadPoolExecutor(concurrency, maxConcurrency, keepAliveSecsonds, TimeUnit.SECONDS, new LinkedBlockingDeque<>(queueSize), rejectedExecutionHandler);
// 放入线程池单例map中
THREAD_POOL_EXECUTOR_MAP.put(poolName, threadPoolExecutor);
}
}
}
return threadPoolExecutor;
}
/**
*
* @author HeJun
* @date 2020年5月14日
* @Version V1.0
* @Description 长轮询
* @param request
* @param requestId
* @param timeOutSeconds
*/
public static void longPolling(HttpServletRequest request, String requestId, long timeOutSeconds) {
//请求异步域对象
AsyncContext asyncContext = request.startAsync();
//关闭长轮询自动超时处理
asyncContext.setTimeout(0);
//消息等待map添加异步域对象
ASYNC_CONTEXT_MAP.put(requestId, asyncContext);
//注册长轮询超时未响应兜底任务
TIME_OUT_CHECKER.schedule(() -> {
try {
writePollingResponse(requestId, R.error(ExceptionEnum.POLLING_TIME_OUT));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}, timeOutSeconds,TimeUnit.SECONDS);
}
/**
* 添加长轮询立即响应触发事件
* @param <T>
* @param requestId
* @param task
* @return
*/
public static <T> boolean addLongPollingRespEvent(String requestId, LongPollingTask<T> task) {
return LONG_POLLING_EVENT_QUEUE.add(new LongPollingEvent<T>(requestId, task));
}
/**
* 长轮询触发响应事件类
* @param <T>
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
static class LongPollingEvent<T> {
// AsyncContext对象id
private String requestId;
// 待处理内容的处理任务
private LongPollingTask<T> task;
}
/**
* 待处理内容的处理任务接口
* @param <T>
*/
public static interface LongPollingTask<T> {
R<T> execute();
}
/**
*
* @author HeJun
* @date 2021年4月28日
* @Version V1.0
* @Description 获得指定名字的线程池,不存在则创建,线程安全
* @param requestId
* @param resp
* @return
*/
private static void writePollingResponse(String requestId, R<?> resp) throws IOException {
if (ASYNC_CONTEXT_MAP.get(requestId) != null) {
synchronized (requestId.intern()){
AsyncContext asyncContext = ASYNC_CONTEXT_MAP.get(requestId);
if (asyncContext != null) {
log.info("事件触发长轮询回复----,requestId={},resp={}",requestId,resp);
ServletResponse response = asyncContext.getResponse();
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("utf-8");
response.getWriter().append(JSON.toJSONString(resp));
asyncContext.complete();
ASYNC_CONTEXT_MAP.remove(requestId);
}else {
log.info("长轮询已回复,超时回复终止----,requestId={},resp={}",requestId,resp);
}
}
}else {
log.info("长轮询已回复,超时回复终止----,requestId={},resp={}",requestId,resp);
}
}
/**
* 长轮询事件触发后的处理守护线程,监听长轮询立即回复事件队列
*/
static {
Thread thread = new Thread(() -> {
while (true) {
try {
LongPollingEvent pollingEvent = LONG_POLLING_EVENT_QUEUE.take();
runAsync(() -> {
try {
R<?> resp = pollingEvent.getTask().execute();
writePollingResponse(pollingEvent.getRequestId(), resp);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
});
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}
});
thread.setDaemon(true);
thread.start();
}
}
_
java
@Data
@ApiModel("R统一返回结果对象")
@SuppressWarnings("unchecked")
@Slf4j
public class R<T> {
@ApiModelProperty("是否成功")
private Boolean success;
@ApiModelProperty("返回码")
private Integer code;
@ApiModelProperty("返回消息")
private String message;
@ApiModelProperty("返回数据")
private T data;
public static <T> R<T> error() {
R<T> r = new R<T>();
r.setSuccess(false);
r.setCode(ExceptionEnum.ERROR.getCode());
r.setMessage(ExceptionEnum.ERROR.getMessage());
return r;
}
public static <T> R<T> error(String message) {
R<T> r = new R<T>();
r.setSuccess(false);
r.setCode(ExceptionEnum.ERROR.getCode());
r.setMessage(message);
return r;
}
public static <T> R<T> error(ExceptionEnum e) {
R<T> r = new R<T>();
r.setSuccess(false);
r.setCode(e.getCode());
r.setMessage(e.getMessage());
return r;
}
public static <T> R<T> ok() {
R<T> r = new R<T>();
r.setSuccess(true);
r.setCode(ExceptionEnum.SUCCESS.getCode());
r.setMessage(ExceptionEnum.SUCCESS.getMessage());
return r;
}
public static <T> R<T> ok(T data) {
R<T> r = new R<T>();
r.setSuccess(true);
r.setCode(ExceptionEnum.SUCCESS.getCode());
r.setMessage(ExceptionEnum.SUCCESS.getMessage());
r.setData(data);
return r;
}
public R<T> message(String message) {
this.setMessage(message);
return this;
}
public R<T> code(Integer code) {
this.setCode(code);
return this;
}
}
java
public enum ExceptionEnum {
SUCCESS(200,"成功"),
USER_INPUT_ERROR(400,"用户输入错误"),
SYSTEM_ERROR(500,"系统异常"),
INNER_ERROR(600,"业务异常"),
ERROR(999,"失败"),
USER_NOT_EXIST(701,"用户名不存在"),
PASSWORD_ERROR(702,"密码错误"),
USER_NOT_ENABLED(703,"用户被禁用"),
WITHOUT_LOGIN(704,"未携带授权凭证"),
USER_EXIST(705,"用户名已存在"),
VERIFYCODE_ERRO(706,"验证码错误"),
DATA_FORMAT_ERRO(707,"数据格式错误"),
USER_NOT_EXAMINE(708,"账号待审核"),
USER_FAIL_EXAMINE(709,"账号审核失败"),
POLLING_TIME_OUT(710,"长轮询暂无数据"),
AUTHORITY_DENIED(401,"无此权限"),
IP_DENIED(402,"该IP已被禁用,请联系管理员"),
ACCESS_DENIED(403,"未登录"),
TOKEN_EXPIRED(405,"登陆已过期");
private int code;
private String message;
ExceptionEnum(int code,String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}