共计 4011 个字符,预计需要花费 11 分钟才能阅读完成。
一、背景
最近在做我的项目的过程中,有一个领取的场景,前端须要依据领取的后果,跳转到不同的页面中。而咱们的领取告诉是支付方异步告诉回来的,因而在收回领取申请后
无奈立刻获取到领取后果,此时咱们就须要轮训交易后果,判断是否领取胜利。
二、剖析
要实现后端将领取后果告诉给前端,实现的形式有很多种。
- ajax 轮训
- 长轮训
- websocket
- sse
……
通过思考,最终决定应用 长轮训
来实现。而 Spring 的 DeferredResult
是一个异步申请,正好能够用来实现长轮训。而这个异步是基于 Servlet3
的异步来实现的,在 Spring 中 DeferredResult 后果会另起线程来解决,并不会占用容器 (Tomcat) 的线程,因而还能进步程序的吞吐量。
三、实现要求
前端申请 查问交易办法 (queryOrderPayResult
), 后端将申请阻塞住 3s
,如果在 3s 之内,领取告诉回调(payNotify
) 过去了,那么之前查问交易
的办法立刻返回领取后果,否则返回超时了。
四、后端代码实现
package com.huan.study.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import javax.annotation.PostConstruct;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 订单控制器
*
* @author huan.fu 2021/10/14 - 上午 9:34
*/
@RestController
public class OrderController {private static final Logger log = LoggerFactory.getLogger(OrderController.class);
private static volatile ConcurrentHashMap<String, DeferredResult<String>> DEFERRED_RESULT = new ConcurrentHashMap<>(20000);
private static volatile AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);
@PostConstruct
public void printRequestCount() {Executors.newSingleThreadScheduledExecutor()
.scheduleAtFixedRate(() -> {log.error("" + ATOMIC_INTEGER.get());
}, 10, 1, TimeUnit.SECONDS);
}
/**
* 查问订单领取后果
*
* @param orderId 订单编号
* @return DeferredResult
*/
@GetMapping("queryOrderPayResult")
public DeferredResult<String> queryOrderPayResult(@RequestParam("orderId") String orderId) {log.info("订单 orderId:[{}]发动了领取", orderId);
ATOMIC_INTEGER.incrementAndGet();
// 3s 超时
DeferredResult<String> result = new DeferredResult<>(3000L);
// 超时操作
result.onTimeout(() -> {DEFERRED_RESULT.get(orderId).setResult("超时了");
log.info("订单 orderId:[{}]发动领取, 获取后果超时了.", orderId);
});
// 实现操作
result.onCompletion(() -> {log.info("订单 orderId:[{}]实现.", orderId);
DEFERRED_RESULT.remove(orderId);
});
// 保留此 DeferredResult 的后果
DEFERRED_RESULT.put(orderId, result);
return result;
}
/**
* 领取回调
*
* @param orderId 订单 id
* @return 领取回调后果
*/
@GetMapping("payNotify")
public String payNotify(@RequestParam("orderId") String orderId) {log.info("订单 orderId:[{}]领取实现回调", orderId);
// 默认后果产生了异样
if ("123".equals(orderId)) {DEFERRED_RESULT.get(orderId).setErrorResult(new RuntimeException("订单产生了异样"));
return "回调解决失败";
}
if (DEFERRED_RESULT.containsKey(orderId)) {Optional.ofNullable(DEFERRED_RESULT.get(orderId)).ifPresent(result -> result.setResult("实现领取"));
// 设置之前 orderId toPay 申请的后果
return "回调解决胜利";
}
return "回调解决失败";
}
}
五、运行后果
1、超时操作
页面申请 http://localhost:8080/queryOrderPayResult?orderId=12345
办法,在 3s 之内没有 DeferredResult#setResult 没有设置后果,间接返回超时了。
2、失常操作
页面申请 http://localhost:8080/queryOrderPayResult?orderId=12345
办法之后,并立刻申请 http://localhost:8080/payNotify?orderId=12345
办法,失去了正确的后果。
六、DeferredResult 运行原理
- Controller 返回一个 DeferredResult 对象,并且把它保留在一个能够拜访的内存队列或列表中。
- Spring Mvc 开始异步解决。
- 同时,DispatcherServlet 和所有配置的过滤器退出申请解决线程,但 Response(响应)放弃关上状态。
- 应用程序从某个线程设置 DeferredResult,Spring MVC 将申请分派回 Servlet 容器。
- DispatcherServlet 再次被调用,并以异步产生的返回值复原解决。
六、注意事项
1、异样的解决
能够通过 @ExceptionHandler
来解决。
2、异步过程中的拦截器。
能够通过 DeferredResultProcessingInterceptor
或者 AsyncHandlerInterceptor
来实现。须要留神看拦截器办法上的正文,有些办法,如果调用了 setResult
等是不会再次执行的。
配置:
/**
* 如果加了 @EnableWebMvc 注解的话, Spring 很多默认的配置就没有了,须要本人进行配置
*
* @author huan.fu 2021/10/14 - 上午 10:39
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
// 默认超时工夫 60s
configurer.setDefaultTimeout(60000);
// 注册 deferred result 拦截器
configurer.registerDeferredResultInterceptors(new CustomDeferredResultProcessingInterceptor());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new CustomAsyncHandlerInterceptor()).addPathPatterns("/**");
}
}
七、残缺代码
https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/spring-deferred-result
八、参考链接
- https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-deferredresult