圖解支付系統的關鍵設計

大家好,我是隱墨星辰,專注境內 / 跨境支付架構設計十餘年。

在前面介紹過支付系統的整體設計,有興趣的讀者可點擊鏈接查看:圖解支付系統整體設計,今天聊聊支付系統的一些關鍵設計細節。

內容主要包括一些支付系統常用的設計,比如領域建模,狀態機,冪等,日誌規範,業務 ID 生成規範,監控,資損防控,支付安全等。這些技術在互聯網其它領域比如電商也是通用的。

這裏只摘錄了部分精華內容出來,但已經能表達最核心的設計理念。

1. 領域建模

領域驅動設計(DDD)思想在支付系統的設計中應用非常廣泛,但需要對行業有非常深刻的理解,否則構建出來的模型穩定性極差,動不動就需要修改核心模型。

收單、結算、支付引擎、會員、商戶服務、渠道網關等各關鍵域都有自己獨特的業務,模型也是不一樣的。因篇幅的關係,只給兩個小示例:

比如會員的三戶模型。

說明:

  1. 客戶是一種社會屬性,代表真實社會的一個實體。比如張三分別使用 1388888888 和 1399999999 兩個手機號在微信支付進行註冊,然後使用同一個身份證做了實名認證,那麼這兩個用戶仍然被歸屬於同一個客戶。

  2. **用戶是一種業務屬性,就是使用產品的身份。**比如張三分別使用 1388888888 和 1399999999 兩個手機號在微信支付進行註冊,那麼就會存在 2 個用戶,這 2 個用戶的業務是獨立的。

  3. **賬戶是一種金融屬性,代表使用資金的身份。**比如張三使用 1388888888 開通了微信支付,經過實名認證後,可以開通餘額,也可以開通基金賬戶。

如果沒有金融屬性,一般就沒有賬戶,比如註冊博客園後,就只有用戶沒有賬戶。

下圖是會員的模型:

說明:

  1. 用戶、客戶、賬戶的關係,詳見上面的三戶模型說明。會員註冊後,就會有一個用戶(UserId),完成實名認證,就會有一個客戶(CustomeId),開通餘額,就會有賬戶(AccountNo)。

  2. 其它的關係,圖中已經很清楚。

支付渠道的模型:

說明:

渠道:對外部渠道做一個抽象,比如國內微信、支付寶、銀聯等,國外的 WPG、MGPS 等。

業務能力:支付、退款、撤銷、請款等能力詳細描述。比如退款有效期、最小限額等。

接口能力:描述接口本身的能力,比如渠道的請求號生成規則。特殊情況下可能有短號問題。

2. 狀態機

狀態機,也稱爲有限狀態機(FSM, Finite State Machine),是一種行爲模型,由一組定義良好的狀態、狀態之間的轉換規則和一個初始狀態組成。它根據當前的狀態和輸入的事件,從一個狀態轉移到另一個狀態。

下圖就是收單子域設計中交易單的狀態機設計。

從圖中可以看到,一共 4 個狀態,每個狀態之間的轉換由指定的事件觸發。因爲有組合支付的情況,所以全額支付成功纔會推進到成功。

常見代碼實現誤區

  1. 經常看到工作多年的研發工程師實現狀態機時,仍然使用 if else 或 switch case 來寫。這是不對的,會讓實現變得複雜,且容易出現問題。

  2. 直接在訂單的領域模型裏面使用 String 來定義,而不是把狀態模式封裝單獨的類。

  3. 直接調用領域模型更新狀態,而不是通過事件來驅動。

  4. 業務不做拆分,一個狀態機包含了所有的業務狀態,比如支付單把退款和撤銷狀態也耦合進去,導致狀態機巨複雜。

下面是一個比較差的狀態機設計:

限於篇幅,這裏無法給出完整示例代碼。有興趣的可以去網上看看,良好的示例有很多。

/**
 * 支付狀態機
 */
public enum PaymentStatus implements BaseStatus {
    INIT("INIT", "初始化"),
    PAYING("PAYING", "支付中"),
    PAID("PAID", "支付成功"),
    FAILED("FAILED", "支付失敗"),
    ;
    // 支付狀態機內容
    private static final StateMachine<PaymentStatus, PaymentEvent> STATE_MACHINE = new StateMachine<>();
    static {
        // 初始狀態
        STATE_MACHINE.accept(null, PaymentEvent.PAY_CREATE, INIT);
        // 支付中
        STATE_MACHINE.accept(INIT, PaymentEvent.PAY_PROCESS, PAYING);
        // 支付成功
        STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_SUCCESS, PAID);
        // 支付失敗
        STATE_MACHINE.accept(PAYING, PaymentEvent.PAY_FAIL, FAILED);
    }
    // 狀態
    private final String status;
    // 描述
    private final String description;
    PaymentStatus(String status, String description) {
        this.status = status;
        this.description = description;
    }
    /**
     * 通過源狀態和事件類型獲取目標狀態
     */
    public static PaymentStatus getTargetStatus(PaymentStatus sourceStatus, PaymentEvent event) {
        return STATE_MACHINE.getTargetStatus(sourceStatus, event);
    }
}

3. 冪等

冪等是針對重複請求的,支付系統一般會面臨以下幾個重複請求的場景:

  1. 用戶多次點擊支付按鈕:在網絡較差或系統過載情況下,用戶由於不確定交易是否完成而重複點擊。

  2. 自動重試機制:系統在超時或失敗時重試請求,可能導致同一支付多次嘗試。

  3. 網絡數據包重複:數據包在網絡傳輸過程中,複製出了多份,導致支付平臺收到多次一模一樣的請求。

  4. 異常恢復:在系統升級或崩潰後,未決事務需要根據已有記錄恢復和完成。內部系統重發操作。

冪等解決方案

所謂業務冪等,就是由各域自己把唯一性的交易 ID 作爲數據庫唯一索引,這樣可以保證不會重複處理。

在數據庫前面可以加一層緩存來提高性能,但是緩存只用於查詢,查到數據認爲就返回冪等成功,但是查不到,需要嘗試插入數據庫,插入成功後再刷新數據到緩存。

爲什麼要使用數據庫的唯一索引做爲兜底,是因爲緩存是可能失效的。

在面臨時經常有候選人只回答到 “使用 redis 分佈式鎖來實現冪等”,這是不對的。因爲緩存有可能失效,分佈式鎖只是用於防併發操作的一種手段,無法根本性解決冪等問題,冪等一定是依賴數據庫的唯一索引解決。

大部分簡單的支付系統只要有業務冪等基本也夠用了。

更復雜的多機房容災情況下,冪等也會非常複雜。比如在 A 機房處理了一筆業務,這時 A 機房掛了,流量切到了 B 機房,B 機房如果沒有相關的冪等數據,那就會冪等失敗。

4. 日誌規範

只要在公司寫過代碼,就一定打印過日誌,但經常發現一些工作多年的工程師打印的日誌也是亂七八糟的。我曾經在一家頭部互聯網公司接手過一個上線一年多的業務,相關日誌一開始就沒有設計好,導致很多監控無法實現,出了線上問題也不知道,最後只能安排工程師返工改造相關的日誌。

我們要明白日誌是用來做什麼的。只是先弄明白做事的目的,我們才能更好把事情做對。在我看來,日誌有兩個核心的作用:1)監控,診斷系統或業務是否存在問題;2)排查問題。

對於監控而言,我們需要知道幾個核心的數據:業務 / 接口的請求量、成功量、成功率、耗時,系統返回碼、業務返回碼,異常信息等。對於排查問題而言,我們需要有出入參、中間處理數據的上下文、報錯的上下文等。

接下來,基於上面的分析,我們就清楚我們應該有幾種日誌:

  1. 接口摘要日誌。監控接口的請求量、成功量、耗時、返回碼等。使用固定格式,需要打印:時間、接口名稱、結果(成功 / 失敗)、返回碼、耗時等基本信息就足夠。

  2. 業務摘要日誌。監控業務的請求量、成功量、核心業務信息、返回碼等。使用固定格式,需要打印:時間、業務類型、上一步狀態、當前狀態、返回碼、核心業務信息(不同業務有不同的核心業務信息,比如流入,就有支付金額 / 退款金額,卡品牌,卡 BIN 等)。

  3. 詳細日誌。用於排查問題,不用於監控。格式不固定。主要包括時間、接口、入參、出參、中間處理數據輸入、異常的堆棧信息等。

  4. 系統異常日誌。同時用於監控。格式固定。需要打印:時間、錯誤碼、錯誤信息、堆棧信息等。

5. 金額處理規範

對於研發經驗不足的團隊而言,經常會犯以下幾種錯誤:

  1. 沒有定義統一的 Money 類,各系統間使用 BigDecimal、double、long 等數據類型進行金額處理及存儲。

  2. 定義了統一的 Money 類,但是寫代碼時不嚴格遵守,仍然有些代碼使用 BigDecimal、double、long 等數據類型進行金額處理。

  3. 手動對金額進行加、減、乘、除運算,單位(元與分)換算。

帶來的後果,通常就是資金損失,再細化一下,最常見的情況有下面 3 種:

  1. 手動做單位換算導致金額被放大或縮小 100 倍。

  2. 比如大家規定傳的是元,但是其中有位同學忘記了,以爲傳的是分,外部渠道要求傳元,就手動乘以 100。或者反過來。

  3. 還有一種情況,部分幣種比如日元最小單元就是元,假如系統約定傳的是分,外部渠道要求傳元,就可能在網關處理時手動乘以 100。

  4. 1 分錢歸屬問題。比如結算給商家,或計算手續費時,碰到除不盡時,使用四捨五入,還是向零舍入,還是銀行家舍入?這取決於財務策略。

  5. 精度丟失。在大金額時,double 有可能會有精度丟失問題。

最佳實踐:

  1. 制定適用於公司業務的 Money 類來統一處理金額。

  2. 在入口網關接收到請求後,就轉換爲 Money 類。

  3. 所有內部應用的金額處理,強制全部使用 Money 類運算、傳輸,禁止自己手動加減乘除、單位換算(比如元到分)。

  4. 數據庫使用 DECIMAL 類型保存,保存單位爲元。

  5. 在出口網關外發時,再根據外部接口文檔要求,轉換成使用指定的單位。有些是元,有些是分(最小貨幣單位)

6. 業務 ID 生成規則

數據庫一般都會設計一個自增 ID 作爲主鍵,同時還會設計一個能唯一標識一筆業務的 ID,這就是所謂的業務 ID(也稱業務鍵)。比如收單域的收單單號。

也有人採用所謂雪花算法,但其實不適用於支付場景。

下面以 32 位的支付系統業務 ID 生成爲例說明。實際應用時可靈活調整。

第 1-8 位:日期。通過單號一眼能看出是哪天的交易。

第 9 位:數據版本。用於單據號的升級。

第 10 位:系統版本。用於內部系統版本升級,尤其是不兼容升級的時候,老業務使用老的系統處理,新業務使用新系統處理。

第 11-13 位:系統標識碼。支付系統內部每個域分配一段,由各域自行再分配給內部系統。比如 010 是收單核心,012 是結算核心。

第 14-15 位:業務標識位。由各域內部定,比如 00-15 代表支付類業務,01 支付,02 預授權,03 請款等。

第 16-17 位:機房位。用於全球化部署。

第 18-19 位:用戶分庫位。支持百庫。

第 20-21 位:用戶分表位。支持百表。

第 22 位:預發生產標識位。比如 0 代表預發環境,1 代表生產環境。

第 23-24 位:預留。各域根據實際情況擴展使用。

第 24-32 位:序列號空間。一億規模,循環使用。一個機房一天一億筆是很大的規模了。如果不夠用,可以擴展到第 24 位,到十億規模。

7. 自動化渠道開關

外部渠道接多了,時不時有個渠道半夜宕機或非預期維護,人工運維成本高,搞個自動化開關。

說明:

  1. 渠道初始爲完全打開。

  2. 當指定時間內成功率低於閾值或指定時間內連續失敗次數大於閾值,就關閉渠道。

  3. 關閉渠道後,撈取最近成功的一筆發起查詢探測,如果查詢失敗,仍然關閉。

  4. 如果查詢成功,說明和渠道的通路是通的,且渠道能提供基本的服務,打開灰度 25%。

  5. 如果灰度 25% 情況下,成功率不達標,仍然關閉。

  6. 如果灰度 25% 情況下,成功率達標,繼續加大灰度比例,直到 100%。

注:上述灰度打開算法還可以優化爲:N*2 算法,其中 N 初始爲 1。舉個例子:先打開 1%,符合要求後,依次打開:2%,4%,8%,16%,32%,64%,100%。通過 7 次操作後,100% 打開。這個算法適合一些體量大的渠道,直接開 25% 如果仍然失敗會影響很大批量的用戶。對於小流量渠道,灰度 1% 可能很久也沒有量進來,不如直接 25% 見效快。

8. 返回碼設計與映射

外部商戶對接支付平臺,支付平臺內部有自己的業務處理,同時還對接了外部的很多渠道,所以需要管理三套返回碼:

基本原則:

支付平臺內部也分了不同域,建議使用一個共同的規範,比如:RS + 子系統編號 + 錯誤級別 + 具體返回碼。具體如下圖所示:

說明:

1-2 位:固定值 RS,Result 縮寫。

3-5 位:子系統編號。比如 001:收銀支付,002:會員等。可方便定位哪個系統出的問題。

6 位:錯誤類或等級。比如:0:正常,1:業務級異常,2:系統級異常。

7-9 位:各業務線自己定。比如:1xx:參數相關,2xx:數據庫相關,3xx:賬戶狀態 / Token 狀態相關等。

這樣的好處在於,每個子域或子系統既有全局的規範,又有自己的靈活性,減少溝通成本。

注意:上面只是寫了 resultCode,還需要有 message,用於描述這個碼代表什麼語義。

踩過的坑很多,大致可以歸爲以下幾類:

  1. 對客映射不準確,導致用戶持續重試失敗,影響用戶體驗。比如 “餘額不足” 或“風控不過”,返回給用戶“系統異常,請重試”,有些用戶就瘋狂地重試。

  2. 外部渠道沒有明確成功或失敗,內部映射成明確成功或失敗,造成資損。比如:

  3. 支付同步請求渠道響應還沒有回來,發起了查詢,查詢返回 “訂單不存在”,直接推進失敗,但最後銀行扣款成功。

  4. 退款同步請求渠道響應返回 “系統異常”,直接推進到失敗,但最後銀行退款成功。

  5. 外部渠道有雙層返回碼,沒有做完整判斷。比如第 1 層只表示接口是否成功(通信層面),第 2 層纔是表示業務是否成功,但是隻判斷了接口層面,就推進了內部訂單的業務狀態。

  6. 返回碼制定過於籠統或太細。

9. 退款自動重試與支付自動重試

在支付系統日常運營工作中,處理退款重發的運營工作量會隨着交易量的增長而線性增長。達到一定程度就需要考慮建設退款自動重試的能力。

下面兩種情況是最常見的需要做退款重發的場景:

  1. 支付平臺發給外部渠道超時。

  2. 外部渠道內部出現 BUG 或臨時系統異常。

當交易量足夠大時,完全人工重發,工作量也是很可觀的。但是我們不能簡單地做系統自動重發,因爲退款有可能是會有資損發生的。比如:用戶支付 100 元,退款 50 元,渠道不支持冪等,渠道已經內部退款成功,但是我們系統因爲邏輯有缺陷又做了重發,那就是退款 2 次成功,資損 50 元。

有些外部渠道的設計不太好,比如只能通過支付請求號去退款,又支持多次部分退,那就相當於沒有冪等能力。

下面是一個簡化版的重發邏輯。

10. 使用多線程併發獲取支付方式

每個用戶在支付系統中綁定了很多支付方式,比如不同的銀行卡,還有內部的餘額,紅包等。每次渲染收銀臺之前,都需要去獲取這些支付系統,彙總後展示給用戶。

一種方式就是串行去獲取,但這明顯會影響用戶體驗。最優的方案當然是使用多線程並行獲取。

11. 同步受理異步處理

用戶提交支付請求後,如果是外部銀行通道,通常耗時都需要幾百到幾千毫秒,如果全鏈路都是同步接口,那麼整個系統的線程很快就被消耗完,且一旦外部銀行出現響應慢的情況,極其容易出現雪崩現象。

所以我們在調用外部銀行扣款時,通常都使用 “同步受理異步處理” 的方案。簡單地說,就是先受理用戶的請求,做基礎校驗,校驗通過後,保存到數據庫,發起一個異步線程請求到外部銀行,然後馬上返回給用戶,前端再發起定時輪詢結果。

當然還可以優化爲在渠道網關做異步化,因爲這裏對接渠道,Hold 住的線程影響面最小。

常見誤區

  1. IO 密集型任務線程池大小設置爲 CPU 核數的 2 倍

哪怕是 IO 密集型服務,我們也不能簡單設置爲 CPU 核數的 2 倍,我們仍然要考慮任務執行耗時,系統設計的最大併發數是多少等因數。

建議爲:系統預期最大併發任務數 * 單任務平均耗時。

注意,這個耗時是指等待外部資源的耗時,不是 CPU 運算耗時。比如外發銀行後,等待外部銀行返回的過程,就是等待時間,基本不消耗 CPU 資源。

  1. 爲什麼設置了最大線程數不生效

曾經碰過一個線上問題:使用 ThreadPoolExecutor,設置了核心線程數,最大線程數,但是線上出現很多超時未處理的任務,但是請求數沒有超過最大線程數。排查很久才發現雖然設置了最大線程數,但是沒有設置隊列大小(LinkedBlockingQueue),那麼它會默認爲 Integer.MAX_VALUE,這基本上可以認爲是無界隊列,也就是請求全部放到了隊列中。

所以讀者如果使用 ThreadPoolExecutor 來配置線程池,最好是根據自己的訴求,把參數設置完整,包括核心線程數,最大線程數,隊列大小,拒絕策略等。比如有些業務超時後已經沒有意義,那就把隊列放小點,拒絕策略爲直接拒絕。

具體的請參考 JAVA 官方文檔。

  1. 直接 new 線程

因爲簡單,有些同學喜歡直接 new 線程。的確,這種方式在簡單場景下是沒有問題的,但是複雜場景下是很容易出問題,且不好排查,建議不要養成這樣的習慣。如果場景真的非常簡單,也建議使用創建固定大小線程池來做,比如 ExecutorService executor = Executors.newFixedThreadPool(n)。

12. Spring 事務模板

爲什麼不使用 @Transaction 註解

以前寫管理平臺的代碼時,經常使用 @Transaction 註解,也就是所謂的聲明式事務,簡單而實用,但是在做支付後,基本上沒有使用 @Transaction,全部使用事務模板來做。主要有兩個考慮:

1)事務的粒度控制不夠靈活,容易出現長事務

@Transactional 註解通常應用於方法級別,這意味着被註解的方法將作爲一個整體運行在事務上下文中。在複雜的支付流程中,需要做各種運算處理,很多前置處理是不需要放在事務裏面的。

而使用事務模板的話,就可以更精細的控制事務的開始和結束,以及更細粒度的錯誤處理邏輯。

@Transactional
public PayOrder process(PayRequest request) {
    validate(request);
    PayOrder payOrder = buildOrder(request);
    save(payOrder);
    // 其它處理
    otherProcess(payOrder);
}

比如上面的校驗,構建訂單,其它處理都不需要放在事務中。

如果把 @Transactional 從 process() 中拿走,放到 save() 方法,也會面臨另外的問題:otherProcess() 依賴數據庫保存成功後才能執行,如果保存失敗,不能執行 otherProcess() 處理。全部考慮進來後,使用註解處理起來就很麻煩。

2)事務傳播行爲的複雜性

@Transactional 註解支持不同的事務傳播行爲,雖然這提供了靈活性,但在實際應用中,錯誤的事務傳播配置可能導致難以追蹤的問題,如意外的事務提交或回滾。

而且經常有多層子函數調用,很容易子函數有一個耗時操作(比如 RPC 調用或請求外部應用),一方面可能出事長事務,另一方面還可能因爲外調拋異步,導致事務回滾,數據庫中都沒有記錄保存。

以前就在生產上碰到過類似的問題,因爲在父方法使用了 @Transactional 註解,子函數拋出異常導致事務回滾,去數據庫找問題單據,竟然沒有記錄,翻代碼一行行看,才發現問題。

事務模板示例:

public class PaymentSystemTransactionTemplate { 
    public static <R> R execute(FlowContext context, Supplier<R> callback) { 
        TransactionTemplate template = context.getTransactionTemplate();
        Assert.notNull(template, "transactionTemplate cannot be null"); 
        PlatformTransactionManager transactionManager = template.getTransactionManager();
        Assert.notNull(transactionManager, "transactionManager cannot be null"); 
        boolean commit = false;
        try {
            TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); // Corrected "TranscationStatus" to "TransactionStatus"
            R result = null;
            try {
                result = callback.get();
            } catch (Exception e) {
                transactionManager.rollback(status); 
                throw e;
            }
            transactionManager.commit(status);
            commit = true;
            return result;
        } finally {
            if (commit) {
                invokeAfterCommit(context);
            }
        }
    }
    private static void invokeAfterCommit(FlowContext context) {
        try {
            context.invokeAfterCommit();
        } catch (Exception e) {
            // 打印日誌
            ... ...
        }
    }
}

13. 分庫分表

當數據量大的時間,分庫分表是再所難免的。一個經典的面試題是:如果分了 100 張表,按商戶來分表,還是按商戶訂單號來分表?如果按商戶分表怎麼解決各表流水數據量平衡問題?如果是按商戶訂單號來分表,商戶想按時間段查詢怎麼辦?

解法有很多種。一種典型的解法,就是線上數據庫按商戶訂單號分表,同時有一個離線庫冗餘一份按商戶號分表的數據,甚至直接使用離線數據平臺的能力,把商戶的按時間段查詢需求從在線庫剝離出來。

網上資料很多,不贅述。

14. 分佈式事務

分佈式事務是個好東西,但是複雜度也高,還經常出現所謂的事務懸掛問題,且雖然各家都號稱簡單易用,對業務代碼侵入少,但事實並非如此。

所以我個人更傾向於避免使用分佈式事務解決方案,而是採用最終一致性來解決。對大部分中小公司來說,最終一致性已經夠用。

網上資料很多,不贅述。

15. 流程引擎

在支付系統中,流程編排隨處可見,比如收銀臺編排獲取各種綁定資產,渠道網關調用外部的渠道進行支付等。可以考慮使用成熟的工具,也可以考慮自研。

下面是一個支付流程編排圖:

Activiti 和 jBPM:配置文件非常繁瑣。

liteflow:配置文件很簡單,但是配置上只知道節點的流轉,核心業務邏輯在代碼裏面,比如什麼條件下推進到成功。

自研流程引擎使用這樣的配置:

whenOrderState(CommonOrderState.INIT) // 初始條件:主單狀態INIT
.onEvent(CommonEvent.CREATE) // 觸發事件:創建
.transitionOrderStateTo(CommonOrderState.PROCESS) // 推進到:支付中
.request(CommonOperation.PAY) // 操作:外發銀行
   .when("subOrder.currentState == SubOrderState.S") // 銀行返回成功推進主單成功
      .transitionOrderStateTo(CommonOrderState.SUCCESS)
   .when("subOrder.currentState == SubOrderState.F") // 銀行返回失敗推進主單失敗
      .transitionOrderStateTo(CommonOrderState.FAIL)
   .when("subOrder.currentState == SubOrderState.U && subOrder.webForm != null") // 推進發消息
      .notifyNode();

通過這個配置,知道初始狀態,觸發條件,如何推進。

16. 支付安全

支付安全核心關注點

支付安全是一個很大的範疇,但我們一般只需要重點關注以下幾個核心點就夠:

  1. 敏感信息安全存儲。

對個人和商戶 / 渠道的敏感信息進行安全存儲。

個人敏感信息包括身份證信息、支付卡明文數據和密碼等,而商戶 / 渠道的敏感信息則涉及商戶登錄 / 操作密碼、渠道證書密鑰等。

  1. 交易信息安全傳輸。

確保客戶端與支付系統服務器之間、商戶系統與支付系統之間、支付系統內部服務器與服務器之間、支付系統與銀行之間的數據傳輸安全。這包括採用加密技術等措施來保障數據傳輸過程中的安全性。

  1. 交易信息的防篡改與防抵賴。

確保交易信息的完整性和真實性,防止交易信息被篡改或者被抵賴。一筆典型的交易,通常涉及到用戶、商戶、支付機構、銀行四方,確保各方發出的信息沒有被篡改也無法被抵賴。

  1. 欺詐交易防範。

識別並防止欺詐交易,包括套現、洗錢等違規操作,以及通過識別用戶信息泄露和可疑交易來保護用戶資產的安全。這一方面通常由支付風控系統負責。

  1. 服務可用性。

防範 DDoS 攻擊,確保支付系統的穩定運行和服務可用性。通過部署防火牆、入侵檢測系統等技術手段,及時發現並應對可能的 DDoS 攻擊,保障支付服務的正常進行。

極簡支付安全大圖

支付安全是一個綜合性的系統工程,除了技術手段外,還需要建立健全的安全制度和合規制度,而後兩者通常被大部分人所忽略。

下圖是一個極簡版的支付安全大圖,包含了支付安全需要考慮的核心要點。

說明:

  1. 制度是基礎。

哪種場景下需要加密存儲,加密需要使用什麼算法,密鑰長度最少需要多少位,哪些場景下需要做簽名驗籤,這些都是制度就明確了的。制度通常分爲行業制度和內部安全制度。行業制度通常是國家層面制定的法律法規,比如《網絡安全法》、《支付業務管理辦法》等。內部安全制度通常是公司根據自身的業務和能力建立的制度,小公司可能就沒有。

  1. 技術手段主要圍繞四個目標:

1)敏感數據安全存儲。

2)交易安全傳輸。

3)交易的完整性和真實性。

4)交易的合法性(無欺詐)。

對應的技術手段有:

17. 資損防控

所有支付公司都對資損(資金損失)看得很重,輕則錢沒了,重則輿論風波,要是引起監管介入,更是吃不了兜着走。

資損本質

資損防控本質

資損防控全生命週期

資損風險分類

資損場景(應對手段限於篇幅無法展開說明)

  1. 金額放大縮小。

  2. 冪等擊穿。

  3. 流水號及短號重複。

  4. 返回碼映射錯誤。

  5. 數據亂序,但是代碼強要求有序列性。

  6. 越權 / 測試環境配置外部渠道生產環境。

  7. 數據庫操作沒有考慮併發。

  8. 狀態機推進邏輯不嚴謹。

  9. 多線程與資源共享導致線程變量污染。

  10. 系統升級沒有考慮到兼容與灰度邏輯。

  11. 運營操作失誤。

18. 監控

監控一般是監控實時交易數據,包括:提交量,成功量,失敗量,成功率。

對應的有:跌 0,跌到一定比例,同比下跌 / 上升,環比下跌 / 上升等。

考慮到支付流量在白天(忙時)大,在晚上(閒時)小,有些渠道流量大,有些渠道流量小等複雜場景,如果想做要到噪音少(告警準確),覆蓋全(沒有遺漏),建議使用時間窗口算法來做,可自適應流量大小,通用性更好。

19. 覈對

監控一般只能監控某個域的數據,但是互聯網應用拆成了很多微服務,微服務之間容易出現數據不一致性的問題,需要儘快發現。這個時候就需要用到覈對。

有些公司把覈對也叫對賬,都是一個意思。

實時覈對與離線覈對是最後的底線

一般的支付平臺都會有內部系統之間的兩兩覈對,這種覈對主要是信息流層面的對賬,主要勾兌狀態、金額、筆數等數據的一致性。

再細分,還可以拆成實時對賬和離線對賬。

實時對賬一般就是監聽數據庫的 binlog,當數據有變動時,延時幾秒後請求雙方系統的查詢接口,查到數據後進行對賬。

離線對賬一般就是把生產數據庫的數據定時清洗到離線庫(一般還可以分爲天表和小時表),然後進行對賬。

20. 三層對賬

這裏主要是指支付平臺和外部渠道的對賬。存在三種情況:

  1. 支付平臺和外部渠道的流水數據對不上。

  2. 流水數據對上後,賬單對不上。

  3. 賬單對上後,實際打款對不上。

所以對應就有三層對賬體系來解決這個問題。

第一層是信息流對賬。我方流水和銀行清算文件的流水逐一勾兌。可能會存在長短款情況。

第二層是賬單對賬。就是把我方流水彙總生成我方賬單,然後把銀行流水彙總生成銀行賬單,進行對賬。可能會存在銀行賬單和我方賬單不一致的情況,比如共支付 100 萬,渠道分 2 次打款,一筆 98 萬,一筆 2 萬。

第三層是賬實對賬。就是我方內部記錄的銀行頭寸和銀行真實的餘額是否一致。可能存在我方記錄的頭寸是 220 萬,但是銀行實際餘額只有 200 萬的情況。

21. 結束語

如前面所說,限於篇幅,只從專題文章中摘錄了部分關鍵思路,算是一個引子。意猶未盡的讀者可關注公衆號後續推出的文章。

深耕境內 / 跨境支付架構設計十餘年,歡迎關注並星標公衆號 “隱墨星辰”,和我一起深入解碼支付系統的方方面面。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/U5uWTNl9uiXC9xiz3ATUXg