支付寶是如何處理熱點賬戶高併發轉賬問題的 前言:方案設計前提 1:爲什麼做熱點賬戶設計 2:業界關於熱點賬戶衝扣設計方案 3:幾種方案的對比 4:詳細方案設計

前言:方案設計前提

一般賬務系統對賬戶的衝扣需要滿足以下兩點

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指出的問題。

加筆者微信mingyuan_2018,或者掃碼加羣,即可獲取完整版pdf資料,以及更多技術方案設計+代碼示例+ 面試資料+ 享受美團,阿里,頭條內推福利 ↓↓↓


注意:想去其他互聯網大廠的勿擾,目前只有美團,阿里,頭條的內推通道

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章