架構設計 | 接口冪等性原則,防重複提交Token管理

1、冪等性概念

一、冪等簡介

編程中一個冪等操做的特色是其任意屢次執行所產生的影響均與一次執行的影響相同。就是說,一次和屢次請求某一個資源會產生一樣的做用影響。java

二、HTTP請求

遵循Http協議的請求,愈來愈強調Rest請求風格,能夠更好的規範和理解接口的設計。git

GET:用於獲取資源,不該有反作用,因此是冪等的;github

POST:用於建立資源,重複提交POST請求可能產生兩個不一樣的資源,有反作用不知足冪等性;sql

PUT:用於更新操做,重複提交PUT請求只會對其URL中指定的資源有反作用,知足冪等性;數據庫

DELETE:用於刪除資源,有反作用,但它應該知足冪等性;編程

HEAD:和GET本質是同樣的,但HEAD不含有呈現數據,僅是HTTP頭信息,沒有反作用,知足冪等性;app

OPTIONS:用於獲取當前URL所支持的請求方法,知足冪等性;分佈式

2、場景業務分析

一、訂單支付

03_1

實際開發中,常常會面對訂單支付問題,基本流程以下:ide

  • 客戶端發起訂單支付請求 ;
  • 支付前系統本地相關業務處理 ;
  • 請求第三方支付服務執行扣款;
  • 第三方支付返回處理結果;
  • 本地服務基於支付結果響應客戶端;

該業務流程中要處理至關複雜的問題,好比事務,分佈式事務,接口延遲超時,客戶端重複提交等等,這裏只基於冪等接口角度來看該流程,其餘問題後續再聊。測試

二、冪等接口

當上述流程的支付請求有明確結果的時候:失敗或成功,這樣業務流程都好處理,可是例如支付場景若是請求超時,如何判斷服務的結果狀態:客戶端請求超時,本地服務超時,請求支付超時,支付回調超時,客戶端響應超時等等。

這就須要設計流程化的狀態管理。

三、基礎操做案例

模擬管理上述流程,設計冪等接口:

表結構設計

CREATE TABLE `dp_order_state` (
    `order_id` BIGINT (20) NOT NULL AUTO_INCREMENT COMMENT '訂單id',
    `token_id` VARCHAR (50) DEFAULT NULL COMMENT '防重複提交',
    `state` INT (1) DEFAULT '1' COMMENT '1建立訂單,2本地業務,3支付業務',
    PRIMARY KEY (`order_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '訂單狀態表';

CREATE TABLE `dp_state_record` (
    `id` INT (11) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
    `order_id` BIGINT (20) NOT NULL COMMENT '訂單id',
    `state_dec` VARCHAR (50) DEFAULT NULL COMMENT '狀態描述',
    PRIMARY KEY (`id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8 COMMENT = '狀態記錄表';

模擬業務流程

將訂單建立,本地業務,支付業務,分開分段管理提交。分階段測試異常熔斷的業務。

@Service
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderStateMapper orderStateMapper ;
    @Resource
    private StateRecordMapper stateRecordMapper ;

    @Override
    public OrderState queryOrder(OrderState orderState) {
        Map<String,Object> paramMap = new HashMap<>() ;
        paramMap.put("order_id",orderState.getOrderId());
        List<OrderState> orderStateList = orderStateMapper.selectByMap(paramMap);
        if (orderStateList != null && orderStateList.size()>0){
            return orderStateList.get(0) ;
        }
        return null ;
    }

    @Override
    public boolean createOrder(OrderState orderState) {
        int saveRes = orderStateMapper.insert(orderState);
        if (saveRes > 0){
            saveStateRecord(orderState.getOrderId(),"訂單建立成功");
        }
        return saveRes > 0 ;
    }

    @Override
    public boolean localBiz(OrderState orderState) {
        orderState.setState(2);
        int updateRes = orderStateMapper.updateState(orderState) ;
        if (updateRes > 0){
            saveStateRecord(orderState.getOrderId(),"本地業務成功");
        }
        return updateRes > 0;
    }

    @Override
    public boolean paymentBiz(OrderState orderState) {
        orderState.setState(3);
        int updateRes = orderStateMapper.updateState(orderState) ;
        if (updateRes > 0){
            saveStateRecord(orderState.getOrderId(),"支付業務成功");
        }
        return updateRes > 0;
    }

    private void saveStateRecord (Long orderId,String stateDec){
        StateRecord stateRecord = new StateRecord() ;
        stateRecord.setOrderId(orderId);
        stateRecord.setStateDec(stateDec);
        stateRecordMapper.insert(stateRecord) ;
    }
}

測試接口

根據訂單狀態,分段補償執行未完成的業務,若是該訂單已經完成,屢次提交不影響最終結果。

@Api(value = "OrderController")
@RestController
public class OrderController {

    @Resource
    private OrderService orderService ;

    @PostMapping("/submitOrder")
    public String submitOrder (OrderState orderState){
        OrderState orderState01 = orderService.queryOrder(orderState) ;
        if (orderState01 == null){
            // 正常業務流程
            orderService.createOrder(orderState) ;
            orderService.localBiz(orderState) ;
            orderService.paymentBiz(orderState) ;
        } else {
            switch (orderState01.getState()){
                case 1:
                    // 訂單建立成功:後推執行本地和支付業務
                    orderService.localBiz(orderState01) ;
                    orderService.paymentBiz(orderState01) ;
                    break ;
                case 2:
                    // 訂單本地業務成功:後推執行支付業務
                    orderService.paymentBiz(orderState01) ;
                    break ;
                default:
                    break ;
            }
        }
        return "success" ;
    }
}

絮叨一句:實際開發中,該流程是不會由頁面屢次提交完成,訂單是不能重複提交的,下面會演示如何控制,這裏業務是執行後推到完成,也可能業務向前清理,把整個流程置爲失敗,這裏涉及關鍵狀態判斷,要選取一個狀態做爲成功或失敗的標識,判斷後續操做流程。在分佈式系統中這種複雜流程最難處理的是分佈式事務,最終一致性問題,後續再聊。

3、接口重複提交

一、表單重複提交

在實際狀況中,接口若是處理時間過長,用戶可能會點擊屢次提交按鈕,致使數據重複。

常見的一個解決方案:在表單提交中隱藏一個token_id參數,一塊兒提交到接口服務中,數據庫存儲訂單和關聯的tokenId,若是屢次提交,直接返回頁面提示信息便可。

二、演示案例

訂單關聯Token查詢

@Service
public class OrderServiceImpl implements OrderService {
    @Override
    public Boolean queryToken(OrderState orderState) {
        Map<String,Object> paramMap = new HashMap<>() ;
        paramMap.put("order_id",orderState.getOrderId());
        paramMap.put("token_id",orderState.getTokenId());
        List<OrderState> orderStateList = orderStateMapper.selectByMap(paramMap);
        return orderStateList.size() > 0 ;
    }
}

測試接口

@RestController
public class OrderController {
    @Resource
    private OrderService orderService ;

    @PostMapping("/repeatSub")
    public String repeatSub (OrderState orderState){
        boolean flag = orderService.queryToken(orderState) ;
        if (flag){
            return "請勿重複提交訂單" ;
        }
        return "success" ;
    }
}

4、源代碼地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent
相關文章
相關標籤/搜索