詳解Spring DeferredResult異步操作使用場景

為什麼使用DeferredResult?

API接口需要在指定時間內將異步操作的結果同步返回給前端時;

Controller處理耗時任務,並且需要耗時任務的返回結果時;

當一個請求到達API接口,如果該API接口的return返回值是DeferredResult,在沒有超時或者DeferredResult對象設置setResult時,接口不會返回,但是Servlet容器線程會結束,DeferredResult另起線程來進行結果處理(即這種操作提升瞭服務短時間的吞吐能力),並setResult,如此以來這個請求不會占用服務連接池太久,如果超時或設置setResult,接口會立即返回。

使用DeferredResult的流程:

  • 瀏覽器發起異步請求
  • 請求到達服務端被掛起
  • 向瀏覽器進行響應,分為兩種情況:
    • 調用DeferredResult.setResult(),請求被喚醒,返回結果
    • 超時,返回一個你設定的結果
  • 瀏覽得到響應,再次重復1,處理此次響應結果

給人一種異步處理業務,但是卻同步返回的感覺。

場景

瀏覽器向A系統發起請求,該請求需要等到B系統(如MQ)給A推送數據時,A才會立刻向瀏覽器返回數據;

如果指定時間內B未給A推送數據,則返回超時。

Demo代碼

接口代碼:

/get是調用A系統的接口返回數據;

/result模擬B系統向A推送數據進行setResult。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
 
 
@RestController
@RequestMapping(value = "/deferred-result")
public class DeferredResultController {
 
    @Autowired
    private DeferredResultService deferredResultService;
 
    /**
     * 為瞭方便測試,簡單模擬一個
     * 多個請求用同一個requestId會出問題
     */
    private final String requestId = "haha";
 
 
    @GetMapping(value = "/get")
    public DeferredResult<DeferredResultResponse> get(@RequestParam(value = "timeout", required = false, defaultValue = "10000") Long timeout) {
        DeferredResult<DeferredResultResponse> deferredResult = new DeferredResult<>(timeout);
 
        deferredResultService.process(requestId, deferredResult);
 
        return deferredResult;
    }
 
    /**
     * 設置DeferredResult對象的result屬性,模擬異步操作
     * @param desired
     * @return
     */
    @GetMapping(value = "/result")
    public String settingResult(@RequestParam(value = "desired", required = false, defaultValue = "成功") String desired) {
        DeferredResultResponse deferredResultResponse = new DeferredResultResponse();
        if (DeferredResultResponse.Msg.SUCCESS.getDesc().equals(desired)){
            deferredResultResponse.setCode(HttpStatus.OK.value());
            deferredResultResponse.setMsg(desired);
        }else{
            deferredResultResponse.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
            deferredResultResponse.setMsg(DeferredResultResponse.Msg.FAILED.getDesc());
        }
        deferredResultService.settingResult(requestId, deferredResultResponse);
 
        return "Done";
    }
}
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.async.DeferredResult;
 
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
 
@Service
public class DeferredResultService {
 
    private Map<String, Consumer<DeferredResultResponse>> taskMap;
 
    public DeferredResultService() {
        taskMap = new ConcurrentHashMap<>();
    }
 
    /**
     * 將請求id與setResult映射
     *
     * @param requestId
     * @param deferredResult
     */
    public void process(String requestId, DeferredResult<DeferredResultResponse> deferredResult) {
        // 請求超時的回調函數
        deferredResult.onTimeout(() -> {
            taskMap.remove(requestId);
            DeferredResultResponse deferredResultResponse = new DeferredResultResponse();
            deferredResultResponse.setCode(HttpStatus.REQUEST_TIMEOUT.value());
            deferredResultResponse.setMsg(DeferredResultResponse.Msg.TIMEOUT.getDesc());
            deferredResult.setResult(deferredResultResponse);
        });
 
        Optional.ofNullable(taskMap)
                .filter(t -> !t.containsKey(requestId))
                .orElseThrow(() -> new IllegalArgumentException(String.format("requestId=%s is existing", requestId)));
 
        // 這裡的Consumer,就相當於是傳入的DeferredResult對象的地址
        // 所以下面settingResult方法中"taskMap.get(requestId)"就是Controller層創建的對象
        taskMap.putIfAbsent(requestId, deferredResult::setResult);
    }
 
    /**
     * 這裡相當於異步的操作方法
     * 設置DeferredResult對象的setResult方法
     *
     * @param requestId
     * @param deferredResultResponse
     */
    public void settingResult(String requestId, DeferredResultResponse deferredResultResponse) {
        if (taskMap.containsKey(requestId)) {
            Consumer<DeferredResultResponse> deferredResultResponseConsumer = taskMap.get(requestId);
            // 這裡相當於DeferredResult對象的setResult方法
            deferredResultResponseConsumer.accept(deferredResultResponse);
            taskMap.remove(requestId);
        }
    }
 
}
import lombok.Data;
import lombok.Getter;
 
@Data
public class DeferredResultResponse {
    private Integer code;
    private String msg;
 
    public enum Msg {
        TIMEOUT("超時"),
        FAILED("失敗"),
        SUCCESS("成功");
 
        @Getter
        private String desc;
 
        Msg(String desc) {
            this.desc = desc;
        }
    }
}

測試

1. 超時

設置超時時間為8s,會一直阻塞8s,如果超時,返回默認超時的結果

 8s過去,沒有setResult,返回超時

2. 進行setResult

在阻塞期間調用setResult,我這裡模擬的是B系統推送數據給A時拋異常失敗的情況,會立刻得到返回結果

總結:

當有前端需要一個耗時操作服務時,可以使用DeferredResult異步機制編寫代碼。如果不用此功能,要麼自己實現類似的功能。要麼是前端輪訓去拉處理的結果,開發量比較大。有瞭DeferredResult可以節省很多我們前後端的開發量。

到此這篇關於詳解Spring DeferredResult異步操作使用場景的文章就介紹到這瞭,更多相關Spring DeferredResult異步內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: