原則:
支付系統思考思路: 先考慮正常case, 先考慮並發[重復支付, 押金退款,提現], 再考慮事務中斷.[重復支付]
能冪等先冪等(需要采用數據庫事務), 不能冪等優先選擇資金安全的方案. [退款和支付的不同處理]
避免事務中斷,盡量采用面向對象嵌套法,每個內部子模塊獲取自己的數據,開頭都需要對需要判斷子模塊實體的狀態,外部系統寫操作放在最后.
對賬對出事務中斷的部分: 修改狀態后人工修復.
角色,用例,模塊,依賴, 抽象,導致. [優惠模塊為什么放置到訂單之前,又雙向依賴訂單,而不是放置再支付中的原因]
從業務抽死剝繭出頭疼地並發場景.
架構重構,千萬不要盲目抽到新的模塊. 沒想明白之前,循環依賴會搞死你,雖然可以依賴倒置的概念,但maven不支持循環依賴. 測試驅動重構.
看得懂,講明白下面的這些文章就出師了:
重構: 支付寶魯肅:支付寶全局架構重構實踐 http://www.infoq.com/cn/presentations/cl-refactor-system-arch/
CAP theorem 理論在 多副本存儲 和 分布式存儲中理解
微服務架構的分布式事務解決方案 龍果學院 含源代碼
tcc github源代碼 https://github.com/search?utf8=%E2%9C%93&q=TCC&type=
TCC是一個理念,其由Atomikos公司的創始人提出,如果想了解其具體內容直接到其官網下載個白皮書看下就好了,任何時候都是看官方文檔才能更准確的獲知答案。不過TCC只是分布式事務中的一個選項,且並非最優選項,這里有篇文章介紹https://github.com/QNJR-GROUP...
https://github.com/QNJR-GROUP/EasyTransaction 含多種模式 和 復合
https://github.com/prontera/spring-cloud-rest-tcc
try 就是凍結. 凍結資金,凍結券,或者直接修改訂單狀態. confirm時啥都不做. tcc三個步驟都需要保證冪等,否則問題很大. 如何做到自動冪等?
通過幾天的資料查找,對解決分布式事務的方法有兩階段提交、支付寶分享的TCC(try-confirm-cancel)和基於消息的最終一致解決方案,其中第一條和第二條雖然也能解決問題,但普遍對第三種基於消息隊列的最終一致解決方案推薦多比較高,所以第一條和第二條可以參考使用。 from 分布式事務方案整合
訂單處理:本地事務
資金賬戶加款、積分賬戶增加積分:TCC型事務(或兩階段提交型事務),實時性要求比較高,數據必須可靠。
會計記賬:異步確保型事務(基於可靠消息的最終一致性,可以異步,但數據絕對不能丟,而且一定要記賬成功)
看看別人對資金安全的總結 http://www.infoq.com/cn/presentations/correctness-ensure-of-funds-in-internet-financialsystem
商戶通知:最大努力通知型事務(按規律進行通知,不保證數據一定能通知成功,但會提供可查詢操作接口進行核對)
業務 | 方案1 | 方案2_黑ma |
支付 | 訂單支付 押金支付 出行卡支付 月卡充值 充值支付 |
bill引擎,回調各業務實體. getFee 時新建實體,還是 success 時新建實體
|
支付和費用變更 | 費用不一致就退回. 問題,券不可退. |
多退,少補的原則. 記錄費用詳情. (拆分同一個費用項 id) |
退款和打款 | 退款記錄再退款表 打款記錄在提現表 |
退款和打款都記錄在訂單表. 打款依賴提現表. 提現表有兩種類型: 司機提現, 退款打款. |
費用,優惠記錄 | 靜態字段標識. 券 id. 后來優惠越來越多,開始意識到需要抽象出 支付表: 含支付類型,支付 id (券 id)等 |
抽象出支付表 |
會計 | 無會計統計 |
基於帳戶地變動去進行會計統計. [ ] 記住: 借代表增加,貸代表減少. 業務收入 1、原借款時:借:其他應收款 2200 |
結合會計
- 記賬
- 復式記賬.
- 會計憑證和賬單.
- 司乘分離的差額. 需要記錄. 不能放在 pay 表里.
- 多個收據對應一筆帳戶變動
- 何時凍結費用變更
- 方案一 嘗試支付后不可更改費用. 凍結費用最早進行.
- 缺點明顯,一旦嘗試支付過就不能變更費用.
- 方案二 凍結費用在網關層回調業務方. 先凍結,然后修改支付成功.
- 缺點:方案復雜,解決萬分之一的問題
- 方案三 不凍結. 修改費用對主體狀態的變更(還需支付,退款) 和 支付成功通知對主體狀態的變更 需要並發加鎖
- 如果支付通知的費用和 現有費用不一致, 狀態仍然為待支付. 判斷期間費用不可變更
- 變更費用后,仍需支付.那么就還需要支付. 如果需要退款.那么就退款.
- 方案一 嘗試支付后不可更改費用. 凍結費用最早進行.
- 支付和退款退的那部分費用是什么?
- 需要和費用模塊聯動.費用歷史.
- 方案一 費用模塊用version, 每次變更保存新的費用項和增加 version. 費用模塊保存 versionList
- 方案二 支付保存費用項 list, 每次自己做計算.
- 方案三 不用關心支付的費用是什么? 只關心總金額. 缺點是不滿足有時候退款,需要根據費用來確定渠道退款順序.(押金等)
- 需要和費用模塊聯動.費用歷史.
- 如何重復支付判斷?
- 方案1: 實體支付成功,我方未成功. 即重復支付.
- 考慮並發情況.
- 兩筆支付都修改實體為支付成功,
- 如何支付溢出判斷?
- 支付成功時,
- 鎖定實體.
- 計算已支付金額..
- 修改自己狀態. 此時其他流水無法修改自己狀態. 已經分布式鎖住了唯一健. 有並發競爭的復雜 case. 表面上只改一個整體的狀態,但實際上是對計算邏輯有競爭.
- 是否支付溢出,那么就退款
- 釋放實體
- 第二筆鎖定實體. 計算已支付金額和已退款金額.
- 代碼能處理正常業務邏輯, 但是能否處理事務中斷后的重試? 如何面對 mq 或者 dubbo 的重試?
- 要做到冪等(見下面條目)
- 如果做不到冪等,那么就選一種沒有資金損失的方案?
- 案例1 重復支付判斷
- 原有邏輯
- 實體未支付, 屬於正常支付成功case. 修改實體為已支付,修改賬單未已支付
- 實體已支付, 屬於重復支付case ,退款. 計算下多支付了多少錢, 多付了哪些費用項, 把多的進行退款.
- 問題: 假設后續流程出錯,整個流程重試, 實體已支付 ,認為屬於重復支付 case,貿然退款就有問題,出現資金損失.
- 解決方案1: 將自賬單和實體統一起來判斷.
-
- 先判斷: 判斷實體已支付,我方已支付. 略過. 實體已支付,我方未支付. 重復支付. 實體已支付,我方支付. 已經修改過狀態
-
- 缺點: 假設正常case下修改實體為已支付后, 事務中斷. 然后被 dubbo 或者 mq 重試.
- 問題: 實體已支付,賬單未支付. 變成重復支付,退款.資金損失.
- 解決方案2: 重試最好導致的問題是本來要退款的變成了不退款. 案例思考: 先改賬單支付,再改實體支付. 事務中斷, 剛好此時實體被另外一筆支付支付. 然后被 dubbo 重試.
- 問題: 賬單已支付,實體已支付. 略過進行下一步. 本來要被退款的被忽略了. 乘客自己會來打電話,或者對賬系統能對出來. 支付的金額(減去退款) !=訂單的總費用
- 原有邏輯
- 案例2 帳戶加款
- 冪等. 只有做到了冪等,才能進行重試. 但是有些情況下,重試是mq,或者 dubbo 自動控制的, 如果沒有肯定出現問題,選擇一種不資損的方案.
- 方案一: 流水+事務 [能夠抵抗微服務拆分,分庫]
- 方案二: 狀態前置判斷法,
- 1.設置事務: 內部操作流水+狀態修改處於一個事務中,寫外部系統無法,可以放在最后?
- 缺點:不能夠抵抗微服務拆分,分庫. 例如:重復支付判斷?
- 2.不設置事務: 相信內部操作流水+狀態修改處於一個系統,會盡可能得處於一個事務中. (要使用面向對象編程法: 形參傳入最上層的 beanWrapper,內部包含了所有對象,所有外部數據都先獲取好,組裝好. (更新除外) 注意: 不要按需獲取,避免調用外部掛掉. ) 否則中間隨意調用外部系統,寫操作除外,可以放在最后?
- 缺點:肯定會出現事務中斷,要選擇一個沒有資金損失的方案.
- 1.設置事務: 內部操作流水+狀態修改處於一個事務中,寫外部系統無法,可以放在最后?
- 雙實體冪等控制(多對1操作) 都在同一個系統和數據庫下,相信極大情況下都是一個事務內的. 可以通過對多和1的 雙狀態判斷來進行冪等控制. [ 例如: 多筆賬單對實體的支付成功邏輯, 基本上不會在實體側保存一份支付成功流水,因為同一個數據庫. 但如果微服務拆分后就不一樣了,微服務拆分引發的冪等重試, 所以這種最好是冪等略過的方式 ,這樣雖然數據會不一致,但是不會引發問題, 重復退款等.]
- 多方支付成功,1方支付成功. 略過 .
- 多方支付未成功,1方支付成功. 說明已經成功 (不考慮中斷在中間的情況, 因為不采用分布式事務,也無從考證)
- 多方
- 雙實體冪等控制(多對1操作變更)冪等判斷, 兩個實體在不同的系統中,必須要求1方記錄多方的操作流水號. 例如 多筆支付重復支付和冪等判斷 , 帳戶的變更冪等判斷.
- 回調時金額不一致的處理. (有了支付溢出專題討論后,這里就簡單很多了)
- 極端場景描述:
- 乘客需支付10元
- 喚起 app 嘗試支付10元.未回調
- 改費用,改成了9元.
- 支付9元
- 10元回調回來.
- 9元回調回來.
- 兩個方案
- 方案一,先設置為支付成功,再由業務方來確定是否全額退款,還是部分退款.
- 如果是全額退款的. 就會照成問題. 和正常支付成功且部分退款的如何區分? 本質上不用區分.就需要看下未退款金額即可.
業務上需要判斷是否已支付. 不能單純地看成功支付的筆數,而是要看總金額.
- 如果是采用多退少補的方式. 如何明確退款對應的費用? 費用 version_from version_to. 押金多少都不動.已賬單為主.
-
- 乘客需支付10元
- 喚起 app 嘗試支付10元.未回調
- 改費用,改成了9元.
- 支付9元
- 10元回調回來. ( 設置實體為支付成功,設置為支付成功,並且退1元. )
- 9元回調回來. ( 設置為成功,設置實體支付成功失敗, 全額退款. 不退.)
-
- 如果是全額退款的. 就會照成問題. 和正常支付成功且部分退款的如何區分? 本質上不用區分.就需要看下未退款金額即可.
- 方案二 及早詢問業務方是否費用變更. 同時凍結費用(越早凍結越好),如果變更如何應對.
- 這樣就會出現
- 多退
- 如果是多退部分,支付成功並部分退款
- 如果是多退全部,支付關閉並全額退款. 這部分退款不需要對應着費用. 做一個標記,是全額原路退,還是業務退款.
- 少補
- 少的部分需要重新交. 交的那部分費用是什么,用 費用項表示. 負數
- 少退
- 如果是少退全部, 支付關閉並全額退款. 這部分退款不需要對應着費用. 做一個標記,是全額原路退,還是業務退款.
- 多退
- 這樣就會出現
- 方案一,先設置為支付成功,再由業務方來確定是否全額退款,還是部分退款.
- 極端場景描述:
- 支付有哪些抽象概念?
- 費用:
- 優惠 (最開始單一的券,就簡單的用字段描述,后面發現優惠多了,只能用關聯表):
- 券
- 權益
- 包月
- 積分
- 哪些是應收款項
- 優惠的部分
- 哪些是顯性成本:
- 優惠的金額. 首單減免,包月.
- 優惠券
- 權益折算成的減免時間,最終和原時間的差價
- 哪些是隱形成本:
- 哪些是隱形收入:
- 包月部分
- 余額部分
- 哪些是用戶關心的支付.
- 余額+線上支付等
- 關閉全額退款還是二次支付?
- 費用是可變的,會導致實體狀態是並發修改的? 回調流程,何時凍結? 是否需要提早凍結,層次很多的情況下.最底層網關層回調時是否就需要凍結?
- 像滴滴, 黑馬一層(可變,有並發),收銀台一層(可變,有並發,一旦並發,就看誰先鎖住,回調先鎖住,狀態無法回溯的話,那么就生成新的實體.最好是如此.),phoenix 一層
1. 抽象不同的階段. ( 引擎模式,更讓人思考, 每個業務增加一個流程點, 越上游收集越好,例子是 回調回來費用變更.)
不同的階段,分類主體不同. 比如計費階段,根據業務分. 渠道階段,根據渠道分.
2. pay 表 只保存乘客支付金額和優惠總額. 1 對多 優惠表
3. 優惠表里保存優惠類型,支付帳戶,總金額.
4. 差額表里保存差額憑證
業務和統一收銀台的交互 ,和 業務和 phoenix 帳戶系統的交互沒什么區別. 但是區別在數據上,都利用了簽名,利用客戶端 以及 簽名的把兩個服務端的交互解耦掉了.(微信就沒解耦,微信和網絡影響服務器資源)
區別: 1. 統一收銀台不僅傳遞了支付金額,還傳遞了各項費用. 面向的 orderId. 一個 orderId 對應多筆支付. 內部封裝掉.
這種做法比較巧妙. 比較適合用在通用組件上.(需要回調業務方. 客戶端回調,解耦服務端)
表結構設計:
函數依賴要存儲.因為可能會變. 老的訂單會有問題. 盧peng 說不要存儲.