2020-10-28

金流與帳務系統開發心法

首先,這篇一定是對的,最多有缺漏些東西而已,掌握此心法來做系統開發,不管程式設計師,系統安全,會計等等,基本上都會滿足需求

以下名詞盡量通用,所以使用"資料集",你開心可以替換成 Table / Collection 之類的單位



心法零:四個人打麻將,打了三天三夜,總額不變

任何交易系統都是,從最小的購物車到交易所的撮合系統,全部都是



心法一:使用"搬移"的方式來做帳,所有金流都有來源與對象,如果沒有,就虛擬出來

假設一個情境:『ATM 入帳給使用者,則建立一筆單,然後使用者的餘額增加』

所以通常用很簡單的直覺來規劃一個系統,通常會有兩個資料集,"入帳紀錄"與"使用者餘額",但這邊就缺了一個對象,"銀行"

銀行>入帳紀錄>使用者餘額

如果當下完成並直接入帳 100 元,則銀行餘額變動為 -100 使用者餘額變動為 +100,這邊有個好處,就是你不用去統整所有入帳紀錄,就能得知全站所有使用者餘額絕對不會超過銀行總扣款,所以你可以從使用者餘額總數出發,從入帳紀錄出發,從銀行餘額出發,三方驗證來檢查這件事情是對的,要簡單就用頭尾即可,那麼繼續往下推

假設『銀行入帳需要人工審核,才能進行使用者加值』的狀況,然後發生了些 ... race condtiion 的問題,幫使用者重複入帳

銀行>入帳紀錄>入帳紀錄(帳務變更狀態)>使用者餘額

所以銀行匯入後,該把 100 元金額從"銀行"移至"銀行_{UserID}_未實現"(或是把UserID換成InvoiceID),等帳務驗證後,再從"銀行_{UserID}_未實現"移至"使用者餘額"內,最後正常結果會與上個 case 相同,但這樣有什麼好處?

如果後台寫爛了,連點兩次幫使用者加了兩次款,則其實是『"銀行_{UserID}_未實現"移至"使用者餘額"』這個過程重複執行兩次,則"銀行_UserID_未實現"這個項目的數值會是負的 ... 這樣應該瞬間能稽核出異常的部分才是,因為假設因為系統問題,使用者重複儲值成了 200 元,全以搬移的方式進行,一定會在某個中介狀態或來源為負值,所以"中介狀態"或是"未實現"或是會計原則中的"在途"的數值就變得異常重要,而非只有單純的搬移概念而已

以這心法應該能推到非常多的實作,類似把任何的餘額變動全部存起來,有來源有對象,則整理出任何一個報表,任何時間點切入應該都變得沒問題才是,且多個面向可以彼此稽核,也會滿足會計與安全的角度才是,稽核的速度也會變得超級快



心法二:自我稽核,串鏈與自省

餘額修正時可以留下一個簡單的 crc32 做檢核碼(請加上固定或變動的 private / salt 字串),類似 "twd" 與 "twd_sum",當然如果多幣別則可以把 checksum 統整為同一個欄位,這樣的好處能防止任何的 injection 攻擊,下個要變更餘額的動作時,必須要先驗證 checksum 後才能進行交易,而歷史資料內上下筆內文,可以如同區塊鏈般取出特徵值後,把目前的結果放在下一筆資料內,保持串鏈連結不被打破,類似 checksum("{ID}_{UserID}_{Amount}_{上一筆的checksum}") 放在某個欄位,這樣就不怕某些資料順序被互換,且順序可由 checksum 得知,以上兩者都完成時,你就不用怕有任何的 injection 和做帳狀態被修正才是



心法三:用新的資料(insert / new item)來解決所有問題

在人工審核的 case 內,假設一個訂單 id 1234 修正狀態從"未處理"改成"完成",但其實你無法保證"完成"前面是否曾經是另外一個"未處理"和另外一個"完成"(race condition>重複入帳),所以這個的重點應該會類似於,新增完成後的任何資料應該都必須是鎖定的狀態,中間如果有變更的行為,會有個虛無飄渺的感覺存在那邊,所以建議使用子資料集來做變更,類似 id 1234 變動時,都必須在另外一個子資料集內有另外一個紀錄,則該子資料集就有所有的變動順序等等,或是全部使用新增另一筆資料來覆蓋原本的資料,類似 parent_id (原始資料是誰) , is_masked (是否已遭到覆蓋) 的方式來進行完成



心法四:給自己留後路,每個值變動前存入上次的結果

在某個使用者一千萬筆資料集面前,已知其中有一筆資料的誤差值為 1,請找出該資料,基本上以上面所有實作還不夠,你還缺了一個帳本的概念,就像銀行的存款簿,都會寫上次餘額(上一行的結餘)、此次修正(存提)、此次結餘,所以如果要完成這個條件,你勢必在任何一張單,有做任何餘額變動時,都必須存下它的上次或此次的最終結餘為多少(這邊包含任何的"在途",因為它們也是餘額的一種),這樣用頭尾兩張單還有自我比對就能比對出那筆誤差的單才是




心法五:分散資料集,排序很重要(使用時間,或是序列完成)

系統一定會擴張到某個很恐怖的程度,系統會分散,而會變動使用者餘額的帳也會到處跑到處長,而這如何收斂?甚至還要歸出整個順序才行?重複怎麼辦?

"時間"很重要,分散式系統中每台主機都不準,所以這邊一定要留存中心伺服器給的時間,通常是 DB 時間,類似 MySQL 可以用 "CURTIME(4)" 取到小數點下四位的時間,其他 DB 甚至可以留存下到 nanosecond 等級,任何交易變動的時間都必須要留存下來(留存不下來的代表你違反"心法三"的規則,請用別的 column 或新的資料來完成)這樣就能很方便的以時序拼回來才是,當然假設你們家系統很小,或是這類交易事務不夠頻繁,則集中用同一個資料集,應該就能完成整個排序的維持這樣就最好了



目前的心得大概就是這些唄,有額外的心得會在進行補充,你不用完成所有的項目,可以抽出認為 ok 的來完成即可,或是使用此心法來完成一道道的內外部防火牆來提前告警哩



可能比內文還重要的補充:(有任何問題都能提出,我會用補充的方式繼續補下去哈哈)



1. 心法一的例子中 "移至"銀行_{UserID}_未實現" 這個欄位勢必會一直修正,因為訂單會 merge 此在途餘額,且以本文規則而言,每次修正都必須存下上次修正的過程與結果,所以可以改為 "銀行_{InvoiceID}_未實現",甚至是

"銀行_{InvoiceType}_{InvoiceKind}_{InvoiceID}_{OwnerType}_{OwnerID}_未實現"

就能大大的減少這事情,這問題的根本原因其實是在途金額的唯一性對唄?因為一張訂單的完成,通常只有成功和失敗,至於需要補單或是部分退款,則可以是先成功後,再打退款紀錄,老實說應該沒有所謂入帳一半的甚至需要修正之類的(同一訂單部分商品取消,其實也能使用先退原單後,再新增部分單完成,然後註記 ID 為原始 ID 就能完成需求),單純在途餘額欄位會變成 N 個,但我認為這對 DB schema 設計來說並不會是困難點才是



2. 心法五的例子中,時間其實只是排序方式,假設有其他欄位,類似 pkey / uuid / serial ... 能完成的話,就拿那個就好了,然而如果沒這前提,繼續假設 DB 為分散式系統,勢必每一台 DB 時間都不準,然而這話題需要分割兩個部分,首先要從 transaction 角度為起點,如果該 DB 設計是所有 transaction 都從同一台來執行的話 ( RDBMS 系列,還有部分 noSQL 實作) 則你應該不用擔心這問題才是,然而如果還是會發生的話,就要自己寫 sort 演算法了,所有的帳應該都可以 merge 在一起在任何一個時間點,則可以找個資料集用 merge 的方式一直算下去,如果發覺結果完全正確單純順序錯帳的話(配合心法四能自省找到錯帳),應該只有 sort 有問題,通常 sort 有問題的深度應該不會超過 10 筆才是,10 筆的前提會是同一使用者在同一幣別同時操作超過了 10 次,然而這問題真正的主因應該會類似:大量批次處理新增資料時順序塞錯了,或是某台主機時間根本就沒有 sync 到 ... 而用遞迴或是一些演算法應該就能重新 sort 完成才是



以上

沒有留言:

張貼留言