支付對賬系統怎麼設計?
https://mp.weixin.qq.com/s/I13R8xydwD-9BS0_fgkOCg
支付對賬系統是整個支付清結算體系中具體基礎性意義的一個環節,是確保支付平臺與各類第三方支付渠道資料一致性的關鍵系統,是商戶資金結算、資金劃撥、資金報表等邏輯準確執行的重要前提。
支付對賬涉及賬單下載處理、核心對賬、差錯處理等諸多細節邏輯,同時根據交易量大小的不同,需要處理的資料量規模也不盡相同,需要在資料處理時進行一些比較細緻地思考。在本文中,作者以單渠道日成功交易訂單量300W左右規模為背景,以較少的系統資源佔用為目標,給大家介紹下系統的實現細節。同時,對於資料不斷增長的情況下,支付對賬系統該如何演進,作者也會結合自身的實踐與思考與大家一起交流探討!
賬單下載&處理
對於公司自建支付系統來說,一般會根據業務的複雜程度不同,對接多個支付渠道。對於網際網路公司而言,常見的渠道會對接支付寶、微信、ApplePay等;而金融類的公司則更多會對接銀聯、易寶、快錢這類銀行卡代收付通道,也會有直接對接銀行渠道的;海外則是如Adyen、Stripe等這類國際支付公司。
各個渠道的賬單介面下載形式,賬單資料格式等會存在不同的差異,而如果完全按照第三方的原始賬單格式儲存,對於後續的賬單資料處理邏輯會比較複雜,並且每增加一個新的支付渠道,賬單儲存表都需要根據渠道賬單結構新建,增加開發工作量,所以並不合理。
為了實現賬單資料的統一格式化,我們需要將各渠道原始的賬單檔案進行統一標準化轉化,同時也需要設計一張相對通用的渠道賬單資料表來儲存不同渠道賬單格式化後的資料,具體結構如下:
另外,在進行賬單資料儲存時為了提高效率,需要將標準賬單檔案格式設計得與表結構一致,這樣在完成資料轉換後可以直接將檔案load/copy到資料庫中,這樣速度會快很多;而考慮資料規模會增長得超級大,這張表也可以儲存在Hive上,只是對於大部分公司的交易量來說,這麼做會有一些技術實現上的成本,目前作者採用的是Postgresql資料庫(版本為9.5.4)作為賬單儲存庫,資料規模大概已在10億條左右,表示暫無壓力。
此外,對於賬單的下載邏輯也需要考慮防重邏輯的,即同一個渠道賬號的同一天的賬單資料不能重複下載和入庫,所以除了儲存具體的賬單資料外,也需要設計一張賬單下載記錄表,用於儲存那個渠道賬號哪一天的下載情況,並在賬單下載任務啟動起根據該表進行防重複下載邏輯判斷。這裡還需要說明下,在下載原始賬單和轉化標準賬單時由於賬單檔案讀寫都是本地磁碟,為了統一集中管理這些賬單檔案、也為了資料安全需要採用統一檔案儲存服務,如可以採用騰訊雲的CFS檔案儲存,或者自己搭建一個資料夾共享服務。
賬單下載記錄中也需要儲存原始賬單檔案及標準賬單檔案的下載位置,平時基本上不會用到,只是為方便日後用於資料問題排查,需要檢索原始檔案時,方便查詢及資料重新載入。
核心對賬邏輯
完成相應渠道的賬單下載任務後,系統就可以根據各個渠道的特點及對賬平臺任務系統的排班邏輯,啟動相應渠道的對賬任務了,例如微信賬單在10點左右開始下載,預計完成時間為10分鐘以內,那麼就可以將微信的對賬任務安排在11點開始執行,以此類推,各個渠道根據自身實際情況確定對賬時點。
從邏輯上看對賬的形式是為了完成A表與B表的集合,如下圖所示:
一般情況下,與第三方支付渠道進行對賬時,會以平臺訂單號作為關聯條件,將賬單表中的資料與支付平臺訂單表的資料進行full join得到一個集合全量,得到的集合會是一個交集、兩個補集。處於交集部分的資料集說明根據訂單號是可以對應上的,但是我們還需要進行訂單金額的比對,如果一致則說明無差錯,對平的資料集按照結算資料要求取賬單資料+平臺訂單資料業務欄位全集,直接生成對賬明細表,而不一致的則需要生成差錯型別為“金額不一致”的差錯資料,並記入對賬差錯資料表。
處於賬單資料這一側的補集屬於長款差錯,即這部分資料在第三方賬單中存在,而在支付平臺訂單成功資料集中未找到,導致這部分差錯資料的原因,可能有跨天交易的情況、也可能是線上測試資料導致、或者屬於系統層面的訂單掉單,在後面差錯處理中會詳解介紹。
而處於支付平臺訂單這一側的補集則屬於短款差錯,即這部分資料在支付平臺成功訂單中存在,而在渠道對賬時點的賬單中不存在,造成這部分差錯的原因有可能是跨天交易情況導致,也有可能是第三方結算錯誤,具體原因需要在設計差錯處理邏輯是根據不同情況進行處理。
在瞭解到對賬的邏輯後,那麼在具體進行系統編碼時應該如何處理呢?
按照上述邏輯,我們需要將賬單資料表與支付平臺訂單表進行full join,但是由於賬單表我們是儲存在Postgresql上的,而支付系統所採用的資料庫可能是Mysql或Oracle,總之,從系統拆分的角度看,一般是不會將對賬處理與線上支付訂單放在一個庫中的,即便在一個庫直接關聯賬單表與支付訂單表也是不明智的,一方面這樣可能會影響實時支付系統的穩定性,另外這些表的資料都是不斷增長的,隨著資料的積累會也會導致對賬資料查詢變慢。
所以,在進行某個渠道對賬時需要根據條件將賬單資料、支付平臺訂單資料分別清洗到兩張中間表中,分別叫做賬單待對賬中間表(A表)、訂單待對賬中間表(B表),然後通過這兩張表進行full join操作,這樣可以確保對賬邏輯不影響別的業務,同時這部分資料可以在日終完成對賬任務後定期清理掉,確保中間表資料規模處於可控狀態。
在程式碼層面通過A表 full join B表後,會得到一個結果集,如果這個結果集資料比較大,系統沒有采用Spark+Hive這種方式話,通過傳統程式設計方式則需要對查詢進行分頁,考慮到資料逐條對賬處理速度較慢,可以一頁獲取資料條數稍多一些,例如一次取5W條,然後在系統內部採用多執行緒方式對資料集分割後並行處理,每個執行緒按照特定的對賬邏輯執行,得到對賬明細結果集或差錯結果集後,批量存入對賬資料庫。
核心參考程式碼如下:
public boolean execute(ShardingContext shardingContext) {
boolean exeResult = true;
long startTime = System.currentTimeMillis();
String paramValue = shardingContext.getJobParameter();
CheckRequest checkRequest = (CheckRequest) JsonUtils.json2Object(paramValue, MbkCheckRequest.class);//對賬批次請求資訊
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("batchNo", checkRequest.getBatchNo());//批次號
paramMap.put("channel", checkRequest.getChannel());//渠道
paramMap.put("tradeType", checkRequest.getTradeType());//交易型別
//關閉PG執行計劃解釋(PG9問題)
unionCheckOrderMapper.enableNestloopToOff();
int totalCount = unionCheckOrderMapper.countByMap(paramMap);//總數
int pageNum = 50000;//pageSize,每頁5W條資料
int fistPage = 1;
int offset = 2;
PostgreSQLPageModel pm = PostgreSQLPageModel.newPageModel(pageNum, fistPage, totalCount);
int page = pm.getTotalPage();
BlockPoolExecutor exec = new BlockPoolExecutor();
exec.init(); //初始化執行緒池
ExecutorService pool = exec.getMbkBlockPoolExecutor();
List<Future<?>> results = Collections.synchronizedList(new ArrayList<Future<?>>());// 平行計算結果資料型別定義
for (int i = 1; i <= page; i++) {//分頁進行資料Fetch
long startTime2 = System.currentTimeMillis();
pm.setCurrentPage(i); //設定當前頁碼
offset = pm.getOffset();
paramMap.put("pageSize", pageNum);
paramMap.put("offset", offset);
List<UnionCheckOrder> outReconList = unionCheckOrderMapper.selectByMap(paramMap);
Map<String, List<?>> entityMap = groupListByAvg(outReconList, 1000);
CountDownLatch latch = new CountDownLatch(entityMap.size());
OutReconProcessTask[] outReconProcessTask = new OutReconProcessTask[entityMap.size()];
Iterator<Map.Entry<String, List<?>>> it = entityMap.entrySet().iterator();
try {
int j = 0;
while (it.hasNext()) {
List<UnionCheckOrder> uList = (List<UnionCheckOrder>) it.next().getValue();
outReconProcessTask[j] = new OutReconProcessTask(latch, uList);
results.add(pool.submit(outReconProcessTask[j]));
j++;
}
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long endTime2 = System.currentTimeMillis();
}
unionCheckOrderMapper.enableNestloopToOn();//開啟PG執行計劃解釋
exec.destory();
long endTime = System.currentTimeMillis();
return exeResult;
}
考慮完成對賬、以及後面會涉及的處理差錯,其結果都是產生對賬明細資料,而對賬明細資料是渠道vs平臺後最為準確的資金資料,對於後續的商戶清分、資金清算、結算都具有重大意義,所以複雜查詢的頻率會比較高,並且資料的使用時間範圍也比較大,對於交易量比較大的公司一年的支付資料量可能達到數十億規模,在資料儲存方面,可以考慮採用TIDB這類分散式關係型資料庫來儲存對賬結果相關的資料,這樣後續的資料處理邏輯效率會提高很多。可能有人會問為什麼不直接使用Hive進行查詢,這是因為Hive的單條查詢和批量查詢的效率是一樣的,所以並不太適合實時查詢,而如果需要將對賬、結算等資料通過管理系統進行管理,涉及的查詢場景比較多,所以綜合考慮使用分散式關係型資料庫會更合適一些。
差錯處理邏輯
對賬邏輯執行完成後,會產生一部分對賬邏輯執行過程中,系統無法匹配的對賬差錯資料,這部分資料會在對賬完成後記錄在對賬差錯資訊表中,差錯資訊表根據差錯型別記錄該筆差錯的詳細資訊,除包括渠道型別、金額、交易時間等關鍵資訊外,還會對差錯進行分類、定義特定的差錯型別編碼。
根據根據不同情況差錯大概可以分為三類:長款、短款、金額錯誤。其中長款根據對賬處理方式的不同可以分為“渠道成功,平臺訂單不存在”、“渠道成功、平臺狀態非成功”兩種情況,從生產實踐上看,因為支付系統中會存在比較多的支付失敗訂單,而國內支付渠道的賬單多數情況下只會提供使用者支付成功的賬單資料,所以在實際進行對賬時,在A中間表、B中間表清洗的都是雙方認為成功的訂單資料,在這種情況下產生的長款型別也就只有“渠道成功,平臺訂單不存在”這種型別了。
而對於短款來說,就是在當日的賬單資料中沒有匹配到,差錯型別也就是“平臺成功,賬單資料不存在”。而“金額不一致”的情況相對少見,主要出現在如微信、支付寶進行營銷活動時,造成的支付平臺訂單金額與第三方不一致的情況;另外,存在訂單部分退款時,如果支付系統訂單模型沒有很好地滿足這類情況的話,也會導致對賬金額不一致的情況發生。
針對不同的差錯型別情況,需要根據根據系統實際情況,設計相應地處理流程。例如,對於長款差錯需要識別其產生的原因,如果是因為交易跨天導致的,例如交易在平臺時間為某日的23:59:59秒,那麼傳送到第三方渠道的時間可能已經是第二天的00:00:01秒這樣,那麼在第一天對賬是會產生一筆短款差錯,此時只需要消除這筆差錯、同時生成對應的對賬明細即可。而如果是因為支付平臺狀態未處理成功,則是系統掉單問題導致,除了正常消除這筆差錯、產生對應的對賬明細資料外,還需要通知支付系統進行狀態更新操作,其涉及的業務邏輯,還需要根據整個支付平臺的流程設計,觸發商戶回撥、或者還需要通知賬務系統進行補記賬操作。
總之,需要根據具體的差錯型別及原因,結合整個支付系統的流程來保證系統間資料的一致性,以下是作者根據通用場景設計、根據不同差錯型別設計的處理流程,供大家參考:
(一)、長款差錯通用處理流程
在以上長款處理流程中,關於跨天交易情況的區分,這裡有一個細節的設計:在判斷完支付訂單狀態為成功後,之所以在判斷是否在T-1天或者T-N天是否存在同一筆匹配的短款差錯之前,判斷是否存在對賬明細的情況,是因為在系統設計時考慮訂單結算的實時性,允許對於短款差錯採取T+1日的處理方式,具體方式會在短款處理流程中說明。
(二)、短款差錯通用處理流程
在以上短款差錯處理流程中,大部分公司為了解決跨天交易的問題,會要求對短款差錯掛賬一天,也就是說要求在T+2日對短款進行處理,因為在T+2日時賬單和對賬出現的長款差錯可以正常抵消處理。在這裡的設計中,允許在T+1日處理,即在沒有第三方賬單資訊的情況下,通過訂單查詢介面進行對賬,並預設將這筆交易的渠道結算時間設定為T+2,對於支付訂單,國內大部分渠道這麼設定是正好可以匹配的,而對於退款可能渠道的結算時間為T+3甚至更長,這種情況會導致對賬明細中的渠道結算時間與實際渠道結算時間存在一定的偏差,而在後面T+3或更長的賬單日時產生的長款差錯處理,可以採取更新策略,即根據實際的渠道結算時間更新對賬明細表的結算時間,這樣就確保了資料渠道結算時間的準確性。
上面的情況如果考慮商戶結算邏輯,可能需要對這種情況做點特殊標記,如設定渠道結算時間、商戶結算時間、或渠道實際結算時間來處理,當然如果為了確保資金結算的穩妥性,也可以採取掛賬T+2/T+N處理的方式,這一點由流程和規則決定,系統只是額外提供了一種邏輯處理方式而已。
(三)、金額不一致差錯處理
正常情況下金額不一致一般以第三方渠道賬單為準,從賬務一致性的角度考慮,可能也需要在流程中加入調賬邏輯,具體的流程可根據具體的產品規則設計。
系統演進化方向
對於對賬系統的演變主要需要從考慮資料的增長、任務資源的合理配置以及系統監控這幾個方向去考慮。如果資料量持續增長到傳統方式已無法處理,可以採用Spark Streming+Hive+Tidb等組合技術方案進行改進。
此外對賬系統是一個以定時任務為主的系統,對於定時任務處理框架的選擇可以採用分散式任務框架(推薦elasticjob/saturn)+自定義任務邏輯的方式綜合處理(如有些任務存在先後順序,如果框架本身不提供這類處理功能,則需要通過業務規則限制)。
而從系統監控角度,由於任務系統不同於實時交易流程,具有執行時間長、資料操作範圍廣泛的特點,除了進行正常的程序級別的監控外,對於各個任務的執行情況,也需要進行比較細緻的監控,這部分可以通過監控打點等方式綜合解決;而對於業務異常日誌的監控則可以通過Sentry等日誌監控工具進行監控。