Spring DeferredResult 異步請求


一、背景

最近在做項目的過程中,有一個支付的場景,前端需要根據支付的結果,跳轉到不同的頁面中。而我們的支付通知是支付方異步通知回來的,因此在發出支付請求后
無法立即獲取到支付結果,此時我們就需要輪訓交易結果,判斷是否支付成功。

二、分析

要實現后端將支付結果通知給前端,實現的方式有很多種。

  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運行原理

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM