聚合支付系統的設計與實現

作者:編程大椰子

原文:https://mdnice.com/writing/81727aec77394ee2a9c99f029cc212e6

支付中心繫統對內爲各個業務線提供統一的支付、退款等服務,對外對接三方支付或銀行服務實現資金的流轉。如下圖:

大部分公司基本都是這樣的架構,主要有以下幾方面的優點:

  1. 形成統一支付服務,降低業務線接入成本及重複研發成本。

  2. 更好更快的支持創新業務,爲公司業務快速發展提供條件。

  3. 更利於構建安全,穩定,可擴展的支付系統。

  4. 利於核心支付數據的沉澱和統一利用。

支付流程

上圖展示了用戶支付的主要流程,分爲三個步驟:

  1. 用戶在業務訂單確認頁,喚起收銀臺頁面。

  2. 用戶在收銀臺頁面選擇支付方式,確認支付,顯示第三方支付頁面,輸入密碼,進行真實支付行爲。

  3. 系統處理用戶支付結果,並通知給用戶及各個相關係統。

下面詳細說下這三個步驟:

1. 喚起商戶收銀臺

  1. 用戶在訂單確認頁點擊 “去支付 “按鈕,調用收銀臺支付下單接口。

  2. 收銀臺將訂單信息緩存併入庫,然後將訂單標識拼裝到收銀臺 URL 上返回給訂單系統。

  3. 訂單系統接收到收銀臺地址跳轉到收銀臺頁面。微信搜索公衆號:Java 後端編程,回覆:java 領取資料 。

上圖展示了兩個業務線(景區業務線,酒店業務線)喚起的收銀臺頁面,大概可以分爲三個區域:

頁面上部分顯示的是支付剩餘時間和應付金額;

中間部分是訂單信息,根據收銀臺定義的數據格式,業務線動態傳遞過來的;

剩餘部分展示的是支付渠道,支付渠道也是業務線根據自己的需求在支付後臺管理系統配置的,想要哪些支付方式以及它們的順序都可以自定義。

2. 用戶確認支付

  1. 用戶在收銀臺頁面選擇支付方式(支付寶支付,微信支付,銀行卡支付等),點擊立即支付按鈕,

  2. 調用支付中心創單接口,支付中心調用三方支付創單接口,同步返回支付信息,支付中心對返回參數進行處理,返回給收銀臺,

  3. 收銀臺攜帶支付中心返回的參數,調用三方接口,喚起三方收銀臺,

  4. 用戶輸入密碼,立即支付。

3. 支付結果處理

  1. 三方系統進行扣款處理,返回收銀臺結果(目前微信支付返回支付中,支付寶返回支付終態(支付成功或支付失敗)),

以下幾個步驟是異步執行的,不分先後。

  1. 收銀臺拿到三方返回的結果,確認用戶已經支付,則分配定時任務輪詢查詢(注意超時時間)後臺支付結果,拿到終態之後跳轉到相應頁面,

  2. 三方系統支付成功後會通知支付中心結果,支付中心做好自身邏輯處理,異步通知訂單系統,然後返回三方系統通知結果,

  3. 如果長時間未收到三方支付結果的通知,爲了防止掉單,支付中心會發起主動查詢來獲取支付最終結果,以保證支付結果的及時更新。

  4. 當然業務線訂單系統爲了防止支付系統出現異步通知問題,也可以定時輪詢支付中心的支付狀態,防止掉單。(圖中未畫)

支付中心繫統一些問題及解決方案

1. 支付訂單超時關閉問題

如果用戶長時間沒有支付,一般都會有一個超時時間(如上圖商戶收銀臺的支付剩餘時間),到達這個超時時間支付單會自動關閉。實現此需求有很多方式,比如:

1. 輪詢 DB

定時輪詢 DB,取出達到超時時間且在支付中的數據,然後執行關閉邏輯。

缺點:1. 存在延遲,取決於定時任務的頻率。2. 影響數據庫性能。

2. JDK 延時隊列(DelayQueue)和時間輪算法

這兩種的算法的實現方式自行搜索。

共同的缺點是 1. 數據易丟失,由於數據存儲在內存中,服務重啓後數據全部消失。2. 有內存限制,如果數據量過大,會出現 OOM 異常。

3. RocketMQ 延時隊列

RocketMQ 支持消息延時發送,社區版不支持任意等級的延遲,目前默認支持 18 個延時等級:

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

比如支付單 30 分鐘過期,在支付單創建成功後發送延遲消息(延時等級爲 16),消費者在 30 分鐘後會拉取到該消息然後執行關閉邏輯。

RocketMQ 延時隊列,無論在數據安全性和及時性都有明顯的優勢,但是目前社區版沒有支持任意級別的延遲。

目前我們使用的是 RocketMQ 延時隊列實現的訂單關閉。

2. 保證支付結果實時性

三方支付系統支付成功後 99.9% 的情況下都會回調通知我們,但也難免有意外,比如三方延遲迴調或者三方系統宕機,爲了保證支付結果的實時性,三方支付也要求我們不能完全依賴於回調接口,所以我們需要定時的調用主動查詢接口來查詢三方的支付結果。這裏我們也是使用的 RocketMQ 延時隊列實現的:

  1. 調用三方支付創單成功後,發送 <支付主動查詢> 延時 MQ 消息。

  2. 消費消息,判斷支付狀態是否到達終態,如果到達終態,則返回處理成功,否則調用三方支付查詢接口,如果支付成功則處理成功業務,返回處理成功。

  3. 如果客戶未支付則判斷是否達到最大的重試次數,如果達到最大重試次數則停止 <支付主動查詢> 的重試,否則解析重試規則,發送下一輪的延時消息。

有三個重要參數,這些參數可以放到配置中心或者配置庫中,

// 初始延遲級別,對應RocketMQ延時等級,比如3對應的延時時間就是10s
private Integer queryInitLevel = 3;

// 重試次數
private Integer queryCount = 6;

// 重試級別,對應RocketMQ延時等級,5s,10s,30s,1m,10m,20m
private String queryDelayLevels = 2,3,4,5,14,15;

支付創單成功後發送延時消息:

public void payQueryTask(String orderNo) {
        PayQueryMessage payQueryMessage = new PayQueryMessage();
        payQueryMessage.setOrderNo(orderNo);

        RetryMessage<PayQueryMessage> retryMessage = new RetryMessage<>();
        retryMessage.setTotalCount(queryCount);
        retryMessage.setDelayLevels(queryDelayLevels);
        retryMessage.setTopic(TopicConst.PAY_QUERY_TOPIC);
        retryMessage.setEventType(RetryEventTypeEnum.PAY_QUERY);
        retryMessage.setEventDesc(RetryEventTypeEnum.PAY_QUERY.getDesc());
        retryMessage.setData(payQueryMessage);

        log.info("{} - 發送消息, retryMessage: {}", LOG_DESC, retryMessage);
        rocketMqProducer.asyncSend(retryMessage.getTopic(), JsonUtil.toJson(retryMessage),
                CodeEnum.codeOf(RocketMQDelayLevelEnum.class, queryInitLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), LOG_DESC);
}

判斷的是否繼續執行任務:

public void sendDelayRetry(RetryMessage<?> retryMessage) {
        int currentCount;
        retryMessage.setCurrentCount(currentCount = retryMessage.getCurrentCount() + 1);
        // 重試達到最大次數
        if (currentCount > retryMessage.getTotalCount()) {
            log.warn("{} - 達到最大次數-{}, 停止重試! retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
            return;
        }
        log.info("{} - 發送重試消息-{}/{}, retryMessage: {}", retryMessage.getEventDesc(), retryMessage.getCurrentCount(), retryMessage.getTotalCount(), JsonUtil.toJson(retryMessage));
        int delayLevel = Integer.parseInt(retryMessage.getDelayLevels().split(",")[retryMessage.getCurrentCount() - 1]);
        rocketMqProducer.asyncSend(retryMessage.getTopic(), retryMessage,
                CodeEnum.codeOf(RocketMQDelayLevelEnum.class, delayLevel).orElse(RocketMQDelayLevelEnum.FiveSeconds), retryMessage.getEventDesc()+", 發送重試消息");
    }

3. 支付結果通知上游容錯

在回調通知上游系統支付結果時,可能會回調失敗,比如網絡異常或上游系統發生短時故障,如果發生這種情況我們單靠簡單的重試是無法完全解決問題的。爲了儘可能的通知成功,我們需要針對沒有通知成功的數據,每隔一段時間通知一次,那這個需求和我們上一個問題差不多,所以可以複用我們的延時重試框架。

流程和保證支付結果實時的差不多,不再贅述。

支付中心繫統中設計模式的應用

模板方法

模板方法模式思想:定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結構即可重定義該算法的某些特定步驟。

簡單的理解就是定義一個模版方法,然後子類實現模版方法中的抽象方法實現個性化的需求。

就支付而言,無論何種支付產品,都是走的同一個支付流程,那我們就可以定義一個支付流程的模板,然後每種支付產品實現這個模板中特定步驟來實現自己的特定需求。

策略

策略模式主要思想:定義一系列的算法,把它們一個個封裝起來,並且使它們可相互替換。

在支付系統中,支付結果主動查詢需要查詢不同的渠道,比如支付寶,微信,銀聯等,每個渠道查詢的方式和參數不盡相同,可以將每種渠道查詢封裝成不同的策略類,然後根據查詢條件來調用不同的策略類。

查詢策略有兩個策略接口,callChannel功能是組裝查詢參數和查詢三方,execute 是處理三方返回的結果統一爲支付中心狀態。(因callChannel有其他地方共用所以分開了兩個方法)。

Spring 下使用策略模式,在項目啓動時,將所有的策略類加載到 Map 中,然後使用時直接在 Map 中獲取。

@Component
public class PayQueryStrategyContext {

    private final Map<String, PayQueryStrategy> payQueryStrategyMap = Maps.newConcurrentMap();

    public PayQueryStrategyContext(Map<String, PayQueryStrategy> payQueryStrategyMap) {
        this.payQueryStrategyMap.clear();
        payQueryStrategyMap.forEach(this.payQueryStrategyMap::put);
    }

    public PayQueryStrategy getPayQuery(@NotNull String channelCode) {
        return this.payQueryStrategyMap.get(OperationTypeConst.Pay_Query + channelCode);
    }
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/VXltCLmJb41LwRnFEo-4yA