圖解支付系統的關鍵設計
大家好,我是隱墨星辰,專注境內 / 跨境支付架構設計十餘年。
在前面介紹過支付系統的整體設計,有興趣的讀者可點擊鏈接查看:圖解支付系統整體設計,今天聊聊支付系統的一些關鍵設計細節。
內容主要包括一些支付系統常用的設計,比如領域建模,狀態機,冪等,日誌規範,業務 ID 生成規範,監控,資損防控,支付安全等。這些技術在互聯網其它領域比如電商也是通用的。
這裏只摘錄了部分精華內容出來,但已經能表達最核心的設計理念。
1. 領域建模
領域驅動設計(DDD)思想在支付系統的設計中應用非常廣泛,但需要對行業有非常深刻的理解,否則構建出來的模型穩定性極差,動不動就需要修改核心模型。
收單、結算、支付引擎、會員、商戶服務、渠道網關等各關鍵域都有自己獨特的業務,模型也是不一樣的。因篇幅的關係,只給兩個小示例:
比如會員的三戶模型。
說明:
-
客戶是一種社會屬性,代表真實社會的一個實體。比如張三分別使用 1388888888 和 1399999999 兩個手機號在微信支付進行註冊,然後使用同一個身份證做了實名認證,那麼這兩個用戶仍然被歸屬於同一個客戶。
-
**用戶是一種業務屬性,就是使用產品的身份。**比如張三分別使用 1388888888 和 1399999999 兩個手機號在微信支付進行註冊,那麼就會存在 2 個用戶,這 2 個用戶的業務是獨立的。
-
**賬戶是一種金融屬性,代表使用資金的身份。**比如張三使用 1388888888 開通了微信支付,經過實名認證後,可以開通餘額,也可以開通基金賬戶。
如果沒有金融屬性,一般就沒有賬戶,比如註冊博客園後,就只有用戶沒有賬戶。
下圖是會員的模型:
說明:
-
用戶、客戶、賬戶的關係,詳見上面的三戶模型說明。會員註冊後,就會有一個用戶(UserId),完成實名認證,就會有一個客戶(CustomeId),開通餘額,就會有賬戶(AccountNo)。
-
其它的關係,圖中已經很清楚。
支付渠道的模型:
說明:
渠道:對外部渠道做一個抽象,比如國內微信、支付寶、銀聯等,國外的 WPG、MGPS 等。
業務能力:支付、退款、撤銷、請款等能力詳細描述。比如退款有效期、最小限額等。
接口能力:描述接口本身的能力,比如渠道的請求號生成規則。特殊情況下可能有短號問題。
2. 狀態機
狀態機,也稱爲有限狀態機(FSM, Finite State Machine),是一種行爲模型,由一組定義良好的狀態、狀態之間的轉換規則和一個初始狀態組成。它根據當前的狀態和輸入的事件,從一個狀態轉移到另一個狀態。
下圖就是收單子域設計中交易單的狀態機設計。
從圖中可以看到,一共 4 個狀態,每個狀態之間的轉換由指定的事件觸發。因爲有組合支付的情況,所以全額支付成功纔會推進到成功。
常見代碼實現誤區
-
經常看到工作多年的研發工程師實現狀態機時,仍然使用 if else 或 switch case 來寫。這是不對的,會讓實現變得複雜,且容易出現問題。
-
直接在訂單的領域模型裏面使用 String 來定義,而不是把狀態模式封裝單獨的類。
-
直接調用領域模型更新狀態,而不是通過事件來驅動。
-
業務不做拆分,一個狀態機包含了所有的業務狀態,比如支付單把退款和撤銷狀態也耦合進去,導致狀態機巨複雜。
下面是一個比較差的狀態機設計:
限於篇幅,這裏無法給出完整示例代碼。有興趣的可以去網上看看,良好的示例有很多。
/**
* 支付狀態機
*/
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. 冪等
冪等是針對重複請求的,支付系統一般會面臨以下幾個重複請求的場景:
-
用戶多次點擊支付按鈕:在網絡較差或系統過載情況下,用戶由於不確定交易是否完成而重複點擊。
-
自動重試機制:系統在超時或失敗時重試請求,可能導致同一支付多次嘗試。
-
網絡數據包重複:數據包在網絡傳輸過程中,複製出了多份,導致支付平臺收到多次一模一樣的請求。
-
異常恢復:在系統升級或崩潰後,未決事務需要根據已有記錄恢復和完成。內部系統重發操作。
冪等解決方案
所謂業務冪等,就是由各域自己把唯一性的交易 ID 作爲數據庫唯一索引,這樣可以保證不會重複處理。
在數據庫前面可以加一層緩存來提高性能,但是緩存只用於查詢,查到數據認爲就返回冪等成功,但是查不到,需要嘗試插入數據庫,插入成功後再刷新數據到緩存。
爲什麼要使用數據庫的唯一索引做爲兜底,是因爲緩存是可能失效的。
在面臨時經常有候選人只回答到 “使用 redis 分佈式鎖來實現冪等”,這是不對的。因爲緩存有可能失效,分佈式鎖只是用於防併發操作的一種手段,無法根本性解決冪等問題,冪等一定是依賴數據庫的唯一索引解決。
大部分簡單的支付系統只要有業務冪等基本也夠用了。
更復雜的多機房容災情況下,冪等也會非常複雜。比如在 A 機房處理了一筆業務,這時 A 機房掛了,流量切到了 B 機房,B 機房如果沒有相關的冪等數據,那就會冪等失敗。
4. 日誌規範
只要在公司寫過代碼,就一定打印過日誌,但經常發現一些工作多年的工程師打印的日誌也是亂七八糟的。我曾經在一家頭部互聯網公司接手過一個上線一年多的業務,相關日誌一開始就沒有設計好,導致很多監控無法實現,出了線上問題也不知道,最後只能安排工程師返工改造相關的日誌。
我們要明白日誌是用來做什麼的。只是先弄明白做事的目的,我們才能更好把事情做對。在我看來,日誌有兩個核心的作用:1)監控,診斷系統或業務是否存在問題;2)排查問題。
對於監控而言,我們需要知道幾個核心的數據:業務 / 接口的請求量、成功量、成功率、耗時,系統返回碼、業務返回碼,異常信息等。對於排查問題而言,我們需要有出入參、中間處理數據的上下文、報錯的上下文等。
接下來,基於上面的分析,我們就清楚我們應該有幾種日誌:
-
接口摘要日誌。監控接口的請求量、成功量、耗時、返回碼等。使用固定格式,需要打印:時間、接口名稱、結果(成功 / 失敗)、返回碼、耗時等基本信息就足夠。
-
業務摘要日誌。監控業務的請求量、成功量、核心業務信息、返回碼等。使用固定格式,需要打印:時間、業務類型、上一步狀態、當前狀態、返回碼、核心業務信息(不同業務有不同的核心業務信息,比如流入,就有支付金額 / 退款金額,卡品牌,卡 BIN 等)。
-
詳細日誌。用於排查問題,不用於監控。格式不固定。主要包括時間、接口、入參、出參、中間處理數據輸入、異常的堆棧信息等。
-
系統異常日誌。同時用於監控。格式固定。需要打印:時間、錯誤碼、錯誤信息、堆棧信息等。
5. 金額處理規範
對於研發經驗不足的團隊而言,經常會犯以下幾種錯誤:
-
沒有定義統一的 Money 類,各系統間使用 BigDecimal、double、long 等數據類型進行金額處理及存儲。
-
定義了統一的 Money 類,但是寫代碼時不嚴格遵守,仍然有些代碼使用 BigDecimal、double、long 等數據類型進行金額處理。
-
手動對金額進行加、減、乘、除運算,單位(元與分)換算。
帶來的後果,通常就是資金損失,再細化一下,最常見的情況有下面 3 種:
-
手動做單位換算導致金額被放大或縮小 100 倍。
-
比如大家規定傳的是元,但是其中有位同學忘記了,以爲傳的是分,外部渠道要求傳元,就手動乘以 100。或者反過來。
-
還有一種情況,部分幣種比如日元最小單元就是元,假如系統約定傳的是分,外部渠道要求傳元,就可能在網關處理時手動乘以 100。
-
1 分錢歸屬問題。比如結算給商家,或計算手續費時,碰到除不盡時,使用四捨五入,還是向零舍入,還是銀行家舍入?這取決於財務策略。
-
精度丟失。在大金額時,double 有可能會有精度丟失問題。
最佳實踐:
-
制定適用於公司業務的 Money 類來統一處理金額。
-
在入口網關接收到請求後,就轉換爲 Money 類。
-
所有內部應用的金額處理,強制全部使用 Money 類運算、傳輸,禁止自己手動加減乘除、單位換算(比如元到分)。
-
數據庫使用 DECIMAL 類型保存,保存單位爲元。
-
在出口網關外發時,再根據外部接口文檔要求,轉換成使用指定的單位。有些是元,有些是分(最小貨幣單位)
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. 自動化渠道開關
外部渠道接多了,時不時有個渠道半夜宕機或非預期維護,人工運維成本高,搞個自動化開關。
說明:
-
渠道初始爲完全打開。
-
當指定時間內成功率低於閾值或指定時間內連續失敗次數大於閾值,就關閉渠道。
-
關閉渠道後,撈取最近成功的一筆發起查詢探測,如果查詢失敗,仍然關閉。
-
如果查詢成功,說明和渠道的通路是通的,且渠道能提供基本的服務,打開灰度 25%。
-
如果灰度 25% 情況下,成功率不達標,仍然關閉。
-
如果灰度 25% 情況下,成功率達標,繼續加大灰度比例,直到 100%。
注:上述灰度打開算法還可以優化爲:N*2 算法,其中 N 初始爲 1。舉個例子:先打開 1%,符合要求後,依次打開:2%,4%,8%,16%,32%,64%,100%。通過 7 次操作後,100% 打開。這個算法適合一些體量大的渠道,直接開 25% 如果仍然失敗會影響很大批量的用戶。對於小流量渠道,灰度 1% 可能很久也沒有量進來,不如直接 25% 見效快。
8. 返回碼設計與映射
外部商戶對接支付平臺,支付平臺內部有自己的業務處理,同時還對接了外部的很多渠道,所以需要管理三套返回碼:
-
提供給商戶 OpenAPI 使用的返回碼:這塊可以直接參考微信支付、支付寶等機構的門戶網站。
-
內部各應用使用的標準返回碼:用於內部業務的處理。
-
渠道返回碼:外部渠道提供的返回碼,每個渠道都不一樣,需要映射到內部標準返回碼。
基本原則:
-
制定統一返回碼規範:在團隊或公司層面制定統一的返回碼規範,明確各個返回碼的含義,確保各模塊一致性。
-
嚴格遵守返回碼定義:研發人員在編碼時,應嚴格按照規範返回對應的返回碼,確保返回碼與實際狀態匹配。明確成功才推進成功,明確失敗才推進失敗,其它全部按 “未知” 處理。
-
區分接口 / 通信成功與業務成功。
-
流入到平臺的(支付、充值等),謹慎映射到成功。從平臺流出的(提現,代發等),謹慎映射到失敗。
支付平臺內部也分了不同域,建議使用一個共同的規範,比如:RS + 子系統編號 + 錯誤級別 + 具體返回碼。具體如下圖所示:
說明:
1-2 位:固定值 RS,Result 縮寫。
3-5 位:子系統編號。比如 001:收銀支付,002:會員等。可方便定位哪個系統出的問題。
6 位:錯誤類或等級。比如:0:正常,1:業務級異常,2:系統級異常。
7-9 位:各業務線自己定。比如:1xx:參數相關,2xx:數據庫相關,3xx:賬戶狀態 / Token 狀態相關等。
這樣的好處在於,每個子域或子系統既有全局的規範,又有自己的靈活性,減少溝通成本。
注意:上面只是寫了 resultCode,還需要有 message,用於描述這個碼代表什麼語義。
踩過的坑很多,大致可以歸爲以下幾類:
-
對客映射不準確,導致用戶持續重試失敗,影響用戶體驗。比如 “餘額不足” 或“風控不過”,返回給用戶“系統異常,請重試”,有些用戶就瘋狂地重試。
-
外部渠道沒有明確成功或失敗,內部映射成明確成功或失敗,造成資損。比如:
-
支付同步請求渠道響應還沒有回來,發起了查詢,查詢返回 “訂單不存在”,直接推進失敗,但最後銀行扣款成功。
-
退款同步請求渠道響應返回 “系統異常”,直接推進到失敗,但最後銀行退款成功。
-
外部渠道有雙層返回碼,沒有做完整判斷。比如第 1 層只表示接口是否成功(通信層面),第 2 層纔是表示業務是否成功,但是隻判斷了接口層面,就推進了內部訂單的業務狀態。
-
返回碼制定過於籠統或太細。
9. 退款自動重試與支付自動重試
在支付系統日常運營工作中,處理退款重發的運營工作量會隨着交易量的增長而線性增長。達到一定程度就需要考慮建設退款自動重試的能力。
下面兩種情況是最常見的需要做退款重發的場景:
-
支付平臺發給外部渠道超時。
-
外部渠道內部出現 BUG 或臨時系統異常。
當交易量足夠大時,完全人工重發,工作量也是很可觀的。但是我們不能簡單地做系統自動重發,因爲退款有可能是會有資損發生的。比如:用戶支付 100 元,退款 50 元,渠道不支持冪等,渠道已經內部退款成功,但是我們系統因爲邏輯有缺陷又做了重發,那就是退款 2 次成功,資損 50 元。
有些外部渠道的設計不太好,比如只能通過支付請求號去退款,又支持多次部分退,那就相當於沒有冪等能力。
下面是一個簡化版的重發邏輯。
10. 使用多線程併發獲取支付方式
每個用戶在支付系統中綁定了很多支付方式,比如不同的銀行卡,還有內部的餘額,紅包等。每次渲染收銀臺之前,都需要去獲取這些支付系統,彙總後展示給用戶。
一種方式就是串行去獲取,但這明顯會影響用戶體驗。最優的方案當然是使用多線程並行獲取。
11. 同步受理異步處理
用戶提交支付請求後,如果是外部銀行通道,通常耗時都需要幾百到幾千毫秒,如果全鏈路都是同步接口,那麼整個系統的線程很快就被消耗完,且一旦外部銀行出現響應慢的情況,極其容易出現雪崩現象。
所以我們在調用外部銀行扣款時,通常都使用 “同步受理異步處理” 的方案。簡單地說,就是先受理用戶的請求,做基礎校驗,校驗通過後,保存到數據庫,發起一個異步線程請求到外部銀行,然後馬上返回給用戶,前端再發起定時輪詢結果。
當然還可以優化爲在渠道網關做異步化,因爲這裏對接渠道,Hold 住的線程影響面最小。
常見誤區
- IO 密集型任務線程池大小設置爲 CPU 核數的 2 倍
哪怕是 IO 密集型服務,我們也不能簡單設置爲 CPU 核數的 2 倍,我們仍然要考慮任務執行耗時,系統設計的最大併發數是多少等因數。
建議爲:系統預期最大併發任務數 * 單任務平均耗時。
注意,這個耗時是指等待外部資源的耗時,不是 CPU 運算耗時。比如外發銀行後,等待外部銀行返回的過程,就是等待時間,基本不消耗 CPU 資源。
- 爲什麼設置了最大線程數不生效
曾經碰過一個線上問題:使用 ThreadPoolExecutor,設置了核心線程數,最大線程數,但是線上出現很多超時未處理的任務,但是請求數沒有超過最大線程數。排查很久才發現雖然設置了最大線程數,但是沒有設置隊列大小(LinkedBlockingQueue),那麼它會默認爲 Integer.MAX_VALUE,這基本上可以認爲是無界隊列,也就是請求全部放到了隊列中。
所以讀者如果使用 ThreadPoolExecutor 來配置線程池,最好是根據自己的訴求,把參數設置完整,包括核心線程數,最大線程數,隊列大小,拒絕策略等。比如有些業務超時後已經沒有意義,那就把隊列放小點,拒絕策略爲直接拒絕。
具體的請參考 JAVA 官方文檔。
- 直接 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. 支付安全
支付安全核心關注點
支付安全是一個很大的範疇,但我們一般只需要重點關注以下幾個核心點就夠:
- 敏感信息安全存儲。
對個人和商戶 / 渠道的敏感信息進行安全存儲。
個人敏感信息包括身份證信息、支付卡明文數據和密碼等,而商戶 / 渠道的敏感信息則涉及商戶登錄 / 操作密碼、渠道證書密鑰等。
- 交易信息安全傳輸。
確保客戶端與支付系統服務器之間、商戶系統與支付系統之間、支付系統內部服務器與服務器之間、支付系統與銀行之間的數據傳輸安全。這包括採用加密技術等措施來保障數據傳輸過程中的安全性。
- 交易信息的防篡改與防抵賴。
確保交易信息的完整性和真實性,防止交易信息被篡改或者被抵賴。一筆典型的交易,通常涉及到用戶、商戶、支付機構、銀行四方,確保各方發出的信息沒有被篡改也無法被抵賴。
- 欺詐交易防範。
識別並防止欺詐交易,包括套現、洗錢等違規操作,以及通過識別用戶信息泄露和可疑交易來保護用戶資產的安全。這一方面通常由支付風控系統負責。
- 服務可用性。
防範 DDoS 攻擊,確保支付系統的穩定運行和服務可用性。通過部署防火牆、入侵檢測系統等技術手段,及時發現並應對可能的 DDoS 攻擊,保障支付服務的正常進行。
極簡支付安全大圖
支付安全是一個綜合性的系統工程,除了技術手段外,還需要建立健全的安全制度和合規制度,而後兩者通常被大部分人所忽略。
下圖是一個極簡版的支付安全大圖,包含了支付安全需要考慮的核心要點。
說明:
- 制度是基礎。
哪種場景下需要加密存儲,加密需要使用什麼算法,密鑰長度最少需要多少位,哪些場景下需要做簽名驗籤,這些都是制度就明確了的。制度通常分爲行業制度和內部安全制度。行業制度通常是國家層面制定的法律法規,比如《網絡安全法》、《支付業務管理辦法》等。內部安全制度通常是公司根據自身的業務和能力建立的制度,小公司可能就沒有。
- 技術手段主要圍繞四個目標:
1)敏感數據安全存儲。
2)交易安全傳輸。
3)交易的完整性和真實性。
4)交易的合法性(無欺詐)。
對應的技術手段有:
-
敏感信息安全存儲:採用加密技術對個人和商戶 / 渠道的敏感信息進行加密存儲,限制敏感信息的訪問權限,防止未授權的訪問和泄露。
-
交易信息安全傳輸:使用安全套接字層(SSL)或傳輸層安全性協議(TLS)等加密技術,確保數據在傳輸過程中的機密性和完整性。
-
交易的完整性和真實性:採用數字簽名技術和身份認證技術確保交易信息的完整性和真實性,對交易信息進行記錄和審計,建立可追溯的交易日誌,以應對可能出現的交易篡改或抵賴情況。
-
防範欺詐交易:通過支付風控系統,及時識別和阻止可疑交易行爲。
-
服務可用性:部署流量清洗設備和入侵檢測系統,及時發現並阻止惡意流量,確保支付系統的穩定運行和服務可用性,抵禦 DDoS 攻擊。
17. 資損防控
所有支付公司都對資損(資金損失)看得很重,輕則錢沒了,重則輿論風波,要是引起監管介入,更是吃不了兜着走。
資損本質
資損防控本質
資損防控全生命週期
資損風險分類
資損場景(應對手段限於篇幅無法展開說明)
-
金額放大縮小。
-
冪等擊穿。
-
流水號及短號重複。
-
返回碼映射錯誤。
-
數據亂序,但是代碼強要求有序列性。
-
越權 / 測試環境配置外部渠道生產環境。
-
數據庫操作沒有考慮併發。
-
狀態機推進邏輯不嚴謹。
-
多線程與資源共享導致線程變量污染。
-
系統升級沒有考慮到兼容與灰度邏輯。
-
運營操作失誤。
18. 監控
監控一般是監控實時交易數據,包括:提交量,成功量,失敗量,成功率。
對應的有:跌 0,跌到一定比例,同比下跌 / 上升,環比下跌 / 上升等。
考慮到支付流量在白天(忙時)大,在晚上(閒時)小,有些渠道流量大,有些渠道流量小等複雜場景,如果想做要到噪音少(告警準確),覆蓋全(沒有遺漏),建議使用時間窗口算法來做,可自適應流量大小,通用性更好。
19. 覈對
監控一般只能監控某個域的數據,但是互聯網應用拆成了很多微服務,微服務之間容易出現數據不一致性的問題,需要儘快發現。這個時候就需要用到覈對。
有些公司把覈對也叫對賬,都是一個意思。
實時覈對與離線覈對是最後的底線
一般的支付平臺都會有內部系統之間的兩兩覈對,這種覈對主要是信息流層面的對賬,主要勾兌狀態、金額、筆數等數據的一致性。
再細分,還可以拆成實時對賬和離線對賬。
實時對賬一般就是監聽數據庫的 binlog,當數據有變動時,延時幾秒後請求雙方系統的查詢接口,查到數據後進行對賬。
離線對賬一般就是把生產數據庫的數據定時清洗到離線庫(一般還可以分爲天表和小時表),然後進行對賬。
20. 三層對賬
這裏主要是指支付平臺和外部渠道的對賬。存在三種情況:
-
支付平臺和外部渠道的流水數據對不上。
-
流水數據對上後,賬單對不上。
-
賬單對上後,實際打款對不上。
所以對應就有三層對賬體系來解決這個問題。
第一層是信息流對賬。我方流水和銀行清算文件的流水逐一勾兌。可能會存在長短款情況。
第二層是賬單對賬。就是把我方流水彙總生成我方賬單,然後把銀行流水彙總生成銀行賬單,進行對賬。可能會存在銀行賬單和我方賬單不一致的情況,比如共支付 100 萬,渠道分 2 次打款,一筆 98 萬,一筆 2 萬。
第三層是賬實對賬。就是我方內部記錄的銀行頭寸和銀行真實的餘額是否一致。可能存在我方記錄的頭寸是 220 萬,但是銀行實際餘額只有 200 萬的情況。
21. 結束語
如前面所說,限於篇幅,只從專題文章中摘錄了部分關鍵思路,算是一個引子。意猶未盡的讀者可關注公衆號後續推出的文章。
深耕境內 / 跨境支付架構設計十餘年,歡迎關注並星標公衆號 “隱墨星辰”,和我一起深入解碼支付系統的方方面面。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/U5uWTNl9uiXC9xiz3ATUXg