一、背景

最近在做项目的过程中,有一个支付的场景,前端需要根据支付的结果,跳转到不同的页面中。而我们的支付通知是支付方异步通知回来的,因此在发出支付请求后
无法立即获取到支付结果,此时我们就需要轮训交易结果,判断是否支付成功。

二、分析

要实现后端将支付结果通知给前端,实现的方式有很多种。

  1. ajax 轮训
  2. 长轮训
  3. websocket
  4. 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运行原理

  1. Controller 返回一个 DeferredResult 对象,并且把它保存在一个可以访问的内存队列或列表中。
  2. Spring Mvc 开始异步处理。
  3. 同时,DispatcherServlet 和所有配置的过滤器退出请求处理线程,但Response(响应)保持打开状态。
  4. 应用程序从某个线程设置 DeferredResult,Spring MVC 将请求分派回 Servlet 容器。
  5. 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

八、参考链接

  1. https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-deferredresult

Spring DeferredResult 异步请求的更多相关文章

  1. Spring Boot 异步请求和异步调用,一文搞定

    一.Spring Boot中异步请求的使用 1.异步请求与同步请求 特点: 可以先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成(例如 ...

  2. Spring MVC 异步请求 Callable

    对于有的请求业务处理流程可能比较耗时,比如长查询,远程调用等,主线程会被一直占用,而tomcat线程池线程有限,处理量就会下降 servlet3.0以后提供了对异步处理的支持,springmvc封装了 ...

  3. Spring MVC 异步处理请求,提高程序性能

    原文:http://blog.csdn.net/he90227/article/details/52262163 什么是异步模式 如何在Spring MVC中使用异步提高性能? 一个普通 Servle ...

  4. Spring注解开发系列Ⅸ --- 异步请求

    一. Servlet中的异步请求 在Servlet 3.0之前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由某一个线程从头到尾负责处理.如果要处理一些 ...

  5. 【Spring学习笔记-MVC-5】利用spring MVC框架,实现ajax异步请求以及json数据的返回

    作者:ssslinppp      时间:2015年5月26日 15:32:51 1. 摘要 本文讲解如何利用spring MVC框架,实现ajax异步请求以及json数据的返回. Spring MV ...

  6. spring mvc 异步 DeferredResult

    当一个请求到达API接口,如果该API接口的return返回值是DeferredResult,在没有超时或者DeferredResult对象设置setResult时,接口不会返回,但是Servlet容 ...

  7. 使用Callable或DeferredResult实现springmvc的异步请求

    使用Callable实现springmvc的异步请求 如果一个请求中的某些操作耗时很长,会一直占用线程.这样的请求多了,可能造成线程池被占满,新请求无法执行的情况.这时,可以考虑使用异步请求,即主线程 ...

  8. 使用Spring AsyncRestTemplate对象进行异步请求调用

    直接上代码: package com.mlxs.common.server.asyncrest; import org.apache.log4j.Logger; import org.springfr ...

  9. spring HandlerInterceptorAdapter拦截ajax异步请求,报错ERR_INCOMPLETE_CHUNKED_ENCODING

    话不多说,直接上正文. 异常信息: Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING 问题描述: 该异常是在页面发送ajax请 ...

随机推荐

  1. Vue element keyup.enter失效不起作用

    解决方式一 添加按键修饰符@keyup.enter.native 解决方式二 把事件绑定到父元素(外框),需注意多个input问题 <div @keyup.enter="login&q ...

  2. C#中的“等待窗体”对话框

    这篇文章向您展示了如何在c#.net Windows窗体应用程序中创建一个等待窗体对话框.创建一个新表单,然后输入您的表单名称为frmWaitForm.接下来,将Label,Progress Bar控 ...

  3. VUE002. 动态使用webpack打包处理后的静态图片路径({ img: require('xxx.png') })

    案例摘要 需求是通过v-for循环渲染数组中数据,其中包括本地包的图片文件.话不多说直接上代码: <a-radio-group class="template-radio"& ...

  4. vue-cli3 创建多页面应用项目

    1.创建vue项目 cmd命令执行  vue create ruc-continuing  创建vue项目,项目名称:ruc-continuing 选择一个 preset(预置项),或自定义: 选择自 ...

  5. 【MySQL】MySQL基础(SQL语句、约束、数据类型)

    数据库的基本概念 什么是数据库? 用于存储和管理数据的仓库 英文单词为:DataBase,简称DB 数据库的好处? 可以持久化存储数据 方便存储和管理数据 使用了统一的方式操作数据库 -- SQL 常 ...

  6. layui日期选择无效的问题

    解决layui引入时间控件无效的问题 - 简书 (jianshu.com) 原因是因为在使用日期选择器的时候,layui源码里有一个laydate.css文件找不到 将下载的文档文件里的css文件夹, ...

  7. 项目需求分析与建议——NABCD模型

    特点一:旧物再利用N:需求:在我们的校园生活中,会遇到许多自己用不到的东西例如,学过的课本.废置的闲置物品等,这些"废物"往往占据着许多空间却不能够发挥自身的价值,通过我们的校园二 ...

  8. scrum项目冲刺_day01总结

    摘要:今日完成任务. 1.app基本框架页面正在进行 2.图像识别正在进行 总任务: 一.appUI页面 二.首页功能: 1.图像识别功能 2.语音识别功能 3.垃圾搜索功能 4.相关新闻爬取 三.我 ...

  9. (转载)Select for update/lock in share mode 对事务并发性影响

    select for update/lock in share mode 对事务并发性影响 事务并发性理解 事务并发性,粗略的理解就是单位时间内能够执行的事务数量,常见的单位是 TPS( transa ...

  10. Jmeter系列(3)- 常用断言之响应断言

    断言的作用 确定请求是有效还是无效的 添加断言 面板模块介绍 Apply to 作用:指定断言作用范围 Main sample and sub-sample:作用于主main sample和子sub- ...