热点账户冲扣设计方案
熱點賬戶沖扣設計方案
前言:方案設計前提
一般賬務系統對賬戶的沖扣需要滿足以下兩點
1:更新賬戶表中的賬戶余額。
2:記錄賬戶明細表中的賬戶更新前余額,賬戶更新后余額,操作金額。
?其中對賬戶表中的余額更新一般是直接update,對賬戶明細表中的操作前金額,操作后金額和操作金額就是對賬戶表update的記錄
1:為什么做熱點賬戶設計
熱點賬戶交易是性能瓶頸,在銀行或者第三方支付系統的賬務數據庫的處理中,數據從一個賬戶轉出,或者有數據轉入一個賬戶,賬戶都會收到記賬請求,并都有一個記賬處理的過程。記賬處理過程主要包括兩部分,一是記錄記賬憑證,二是更新賬戶的余額。為了保證賬戶不被其他請求影響數據的準確性,在進行記賬處理時,會先對賬戶的資源加鎖,記賬處理完畢后會自動釋放鎖。隨著賬務處理業務量的增大,賬務數據庫中的賬戶常常會在瞬間產生多個并發操作,但所有對應的并發線程中只有一個線程能夠持有當前賬戶的資源鎖,其他線程必須等待該鎖被釋放后再逐一進行記賬處理,這樣該賬戶將會被頻繁加鎖釋鎖,使該賬戶成為賬務數據庫熱點,產生性能瓶頸點,嚴重影響賬務數據庫的性能。
?
對于同一賬戶ID來說,由于實際業務需要更新賬戶可用余額和賬戶流水日志,所以單筆沖扣功能是在一個事物中進行操作,任何更新操作都會對數據上行鎖,圖例如下
?
?
2:業界關于熱點賬戶沖扣設計方案
?
1.并發度控制
同一時刻,對同一賬戶修改的請求數越多,這個賬戶的鎖等待問題就越嚴重,所謂并發度控制就是要控制同一時刻對熱點賬戶請求的數量,可以通過控制上游支付系統并發請求數據或者賬務系統處理的并發請求數來實現。這一方案的缺點是對業務是有損的,當熱點賬戶出現的時候,支付或者賬務處理失敗率會增加,用戶的體驗會變差,較大的銀行或者第三方支付公司用地比較少。
?
?
2.匯總明細記賬
實時的交易全部是insert賬務明細(insert的開銷很小,能夠支持高并發。如果基于分布式部署,insert的并發容量理論上可以無限大),然后定時(比如每半個小時)將之前半個小時內的賬務明細sum出一個結算總金額,一筆入賬結算到指定賬戶。這個方案的缺點就是:交易不能實時入賬,其實如果控制好定時匯總入賬的頻度,比如分鐘級,用戶也是可以接受的。這種方式對收單類業務(賬戶加錢)非常實用,但是對支出類業務(賬戶減錢)類來說,有賬戶透支地風險。
?
?
3.緩沖入賬
將實時同步的記賬請求進行異步化,以達到記賬實時性和系統穩定性之間平衡的記賬手段,這就是”削峰填谷“。詳細地講,假如賬務系統對同一個賬戶的處理閾值為100筆/s,24小時不間斷服務(一天能處理86400000筆)。當業務高峰期來臨的時候,熱點賬務的請求數會達到200筆/s。當賬戶的交易低于100筆/秒的時候,賬務系統幾乎還是實時地處理了記賬請求,而當交易大于100筆/秒的時候,賬務系統先返回結果,把賬務處理丟到可靠的處理隊列中,等并發量不大的時候慢慢消化,對用戶來說感受到的體驗還是很快就記賬成功了。 這個方案是有個前提是:熱點賬戶在某幾個高峰時間點需要緩沖記賬來削峰填谷,并且能在日間填完。一旦賬戶的日間交易量暴增,導致日間隊列根本來不及消化,整個隊列越來越長,那就不存在谷可以填,這時候肯定會帶來用戶大量的投訴。另外這種方案對支出類業務(賬戶減錢)來講,也會有賬戶透支地風險
?
?
?
4.子賬戶拆分
具體來講就是創建與熱點賬戶對應的多個影子賬戶,所述影子賬戶與所述賬戶的數據結構相同,將所述影子賬戶設置為隱藏,并將所述賬戶的余額分散至各個影子賬戶。當賬務系統接收到賬務請求的時候,通過前置進行hash分配(具體的hash函數會有更多方案)選擇影子賬戶進行記賬,這樣就將原來對一個賬戶的請求分散到多個影子賬戶中,分散了賬務熱點。 這個方案也有缺點:通過算法選擇的影子賬戶扣款,影子賬戶的余額可能是不足的,但賬戶的總余額是夠的,這樣可能影響賬務處理的成功率。
?
?
5.內存數據庫+緩存入賬
提高單臺數據庫服務器處理能力(I/O,CPU,memory)或者選取內存數據庫實時地處理記賬請求,然后異步地存儲到可靠數據庫上。
?
?
6.升級服務硬件,對CPU內存等進行升級
3:幾種方案的對比
1:并發度控制
對單個賬戶并發操作進行限流降級控制,使得系統健康的完成入賬出賬操作,但是在并發很高的情況下還是會殺死很多正常的沖扣功能,會極大的提高沖扣的失敗率,所以對我們賬務系統來說不是允許的。
2:匯總明細入賬
對賬戶的沖扣操作已流水的形式記錄下來,通過定時job來將出入賬流水更新到業務表中。這種做法對于頻繁的入賬來說性能提高明顯,但是因為沒有對總金額進行校驗,對支出類業務(賬戶減錢)類來說,有賬戶透支地風險。并且對于金額的校驗需要通過流水數據和當前可用余額來判定,有并發問題,計算很難準確。并且我們實際線上業務是【頻繁出賬,低頻入賬】,所以此辦法不可取。
3:緩沖入賬
需要動態判斷流量低峰高峰,維護請求隊列,有賬戶透支地風險,并且異步請求中結果不可控。
4:子賬戶拆分
子賬戶拆分方案中對于子賬戶的扣款進行負載,可以滿足對同一賬戶的高頻訪問負載到其子賬戶上,極大滿足了并發的需求,子賬戶的余額可能是不足的,但賬戶的總余額是夠的,這樣可能影響賬務處理的成功率,并且處理對子賬戶的扣款和入賬來說需要做到金額相對平均比較復雜,對記錄賬戶期初余額期末余額處理涉及到并發,相對復雜。
5:增加硬件處理能力CPU.內存等
備選方案,無法從根本上解決單點賬戶的并發壓力。
6:內存數據庫實時地處理記賬,異步入庫
使用redis做數據前置處理,將數據庫中的熱點賬戶金額初始同步到redis中,然后將操作記錄流水,通過job定時任務刷新流水到業務表。這樣將db和緩存分開極大的加大了并發性能,但是卻衍生出來一個問題如下
假設redis初始金額為100,
(1) 當線程1對redis賬戶金額進行原子減操作時,剩余金額40,并記錄流水表等待異步入賬
(2) 當線程2對redis賬戶金額進行原子減操作時,剩余金額-20,此時金額已經為負,按照業務要求金額不能為負所以必須要做反向操作
(3) 當線程2還沒有對redis余額進行反向操作維護的時候又出現線程3進行充值操作,此時金額又變成-20+100=80,已經出現金額混亂,對業務要求的期初余額期末余額無法準確的滿足,所以對redis的金額進行同時沖扣會帶來余額的并發問題。
?
但是對緩存進行操作和延遲批量流水入賬可以極大的滿足我們對性能的需求,所以在【2.匯總明細記賬】和【5.內存數據庫+緩存入賬】的基礎上進行改良來滿足對我們的業務需求
?
4:詳細方案設計
方案設計前提:
(1):【對賬戶的余額的更新】:準確的更新賬戶余額,不允許出現多扣,少扣等情況。
(2):【對賬戶操作記錄的更新】:準確的記錄賬戶流水表中期初余額,期末余額,操作金額等情況,不允許出現任何的金額錯誤發生。
?
前期準備:
(1) :新增延遲入賬【流水表】,新增入賬,出賬數據先入【流水表】,通過定時任務將【流水表】入賬和出賬數據同步到業務數據表中,并且負責新增入賬數據的緩存同步工作。
下面的方案會對此表統一稱為【流水表】
?
?
(2) :新增【redis】數據結構【SortedSet(有序集合)】 key為【hotspot_account】
?
下面會對這個數據集合稱為【緩存操作記錄】
?
其中score為當前賬戶操作時間【新覆蓋舊】,member為出入賬的賬戶ID。key【hotspot_account】,所有賬戶的入賬出賬操作需要記錄到hotspot_account中,主要是提供給【圖1中定時任務】獲取所有賬戶流水ID。
?
(3)新增【redis】數據結構【SortedSet(有序集合)】 key為【hotspot_account_currentbalance】
下面會對這個數據集合稱為【緩存余額】
?
?
其中:
score為當前賬戶可用余額,【熱點賬戶新操作流程之前需要將數據庫中熱點賬戶的數據同步到hotspot_account_currentbalance中】
member為賬戶ID
?
到此,前期準備工作已經全部結束。
?
當賬戶金額充值新增時:
1:記錄redis操作記錄【hotspot_account】
?
如圖所示紅色數據部分,當賬戶110000056666660010入賬時,插入或更新數據,member=110000056666660010,score為當前時間戳(秒)。
ps:操作指令【ZINCRBY key increment member】,當?key?不存在,或?member?不是?key?的成員時,?ZINCRBY?key?increment?member?等同于?ZADD?key?increment?member?。
2:新增【流水表】,設置入賬狀態為未入賬
?
?
當賬戶金額扣減時:
1:同金額充值相同首先記錄redis操作記錄【hotspot_account】。
2:直接對緩存hotspot_account_currentbalance對應的金額進行扣減。
?
3:定時任務
定時任務的作用是將流水表的數據更新到【賬戶表】,和【流水明細表】,并且設置【流水表中】數據已入賬,同時要將新入賬數據流水到更新【hotspot_account_currentbalance】中的可用賬戶余額,讓扣減操作得以繼續進行。以下操作流程:
?
修改于2019年10月24日:
在第15步的時候目前版本在極端情況下會出現一個問題,當定時任務發現了當前緩存余額<0時,會再從流水表中把數據重新查詢一遍入賬,這里有一個小小的問題,在查詢過程中可能會有部分線程沒有入賬到數據庫,這樣會出現超扣的問題,也就是說線程1在
扣減redis的時候成功了,但是沒有insert到流水表,這個時候又有一個線程2扣減redis的線程扣負了,這個時候定時任務發現余額為負,不應該直接同步余額正確的做法是加鎖等待扣減操作流程執行完畢,由于加鎖的復雜性,所以這里采用一個簡單的辦法,由于扣減redis和mysql操作基本都是瞬時的,所以直接sleep 5 s即可,這個時間足夠發生full gc等一些其他未知因素的耗時了,當然感興趣的小伙伴也可以加鎖去自己拓展實現,會有一定性能上的影響,最后感謝群里小伙伴@Token指出的問題。
?
?
總結
以上是生活随笔為你收集整理的热点账户冲扣设计方案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2012年度十大杰出IT博客之 高俊峰
- 下一篇: [附源码]计算机毕业设计Python+u