前言:方案設(shè)計(jì)前提
一般賬務(wù)系統(tǒng)對(duì)賬戶的沖扣需要滿足以下兩點(diǎn)
1:更新賬戶表中的賬戶余額确沸。
2:記錄賬戶明細(xì)表中的賬戶更新前余額,賬戶更新后余額怎虫,操作金額喊熟。
其中對(duì)賬戶表中的余額更新一般是直接update,對(duì)賬戶明細(xì)表中的操作前金額达皿,操作后金額和操作金額就是對(duì)賬戶表update的記錄
1:為什么做熱點(diǎn)賬戶設(shè)計(jì)
熱點(diǎn)賬戶交易是性能瓶頸,在銀行或者第三方支付系統(tǒng)的賬務(wù)數(shù)據(jù)庫(kù)的處理中天吓,數(shù)據(jù)從一個(gè)賬戶轉(zhuǎn)出,或者有數(shù)據(jù)轉(zhuǎn)入一個(gè)賬戶峦椰,賬戶都會(huì)收到記賬請(qǐng)求龄寞,并都有一個(gè)記賬處理的過程。記賬處理過程主要包括兩部分汤功,一是記錄記賬憑證物邑,二是更新賬戶的余額。為了保證賬戶不被其他請(qǐng)求影響數(shù)據(jù)的準(zhǔn)確性滔金,在進(jìn)行記賬處理時(shí)色解,會(huì)先對(duì)賬戶的資源加鎖,記賬處理完畢后會(huì)自動(dòng)釋放鎖餐茵。隨著賬務(wù)處理業(yè)務(wù)量的增大科阎,賬務(wù)數(shù)據(jù)庫(kù)中的賬戶常常會(huì)在瞬間產(chǎn)生多個(gè)并發(fā)操作,但所有對(duì)應(yīng)的并發(fā)線程中只有一個(gè)線程能夠持有當(dāng)前賬戶的資源鎖忿族,其他線程必須等待該鎖被釋放后再逐一進(jìn)行記賬處理萧恕,這樣該賬戶將會(huì)被頻繁加鎖釋鎖,使該賬戶成為賬務(wù)數(shù)據(jù)庫(kù)熱點(diǎn)肠阱,產(chǎn)生性能瓶頸點(diǎn)票唆,嚴(yán)重影響賬務(wù)數(shù)據(jù)庫(kù)的性能。
對(duì)于同一賬戶ID來說屹徘,由于實(shí)際業(yè)務(wù)需要更新賬戶可用余額和賬戶流水日志走趋,所以單筆沖扣功能是在一個(gè)事物中進(jìn)行操作,任何更新操作都會(huì)對(duì)數(shù)據(jù)上行鎖,圖例如下
2:業(yè)界關(guān)于熱點(diǎn)賬戶沖扣設(shè)計(jì)方案
1.并發(fā)度控制
同一時(shí)刻噪伊,對(duì)同一賬戶修改的請(qǐng)求數(shù)越多簿煌,這個(gè)賬戶的鎖等待問題就越嚴(yán)重氮唯,所謂并發(fā)度控制就是要控制同一時(shí)刻對(duì)熱點(diǎn)賬戶請(qǐng)求的數(shù)量,可以通過控制上游支付系統(tǒng)并發(fā)請(qǐng)求數(shù)據(jù)或者賬務(wù)系統(tǒng)處理的并發(fā)請(qǐng)求數(shù)來實(shí)現(xiàn)姨伟。這一方案的缺點(diǎn)是對(duì)業(yè)務(wù)是有損的惩琉,當(dāng)熱點(diǎn)賬戶出現(xiàn)的時(shí)候,支付或者賬務(wù)處理失敗率會(huì)增加夺荒,用戶的體驗(yàn)會(huì)變差瞒渠,較大的銀行或者第三方支付公司用地比較少。
2.匯總明細(xì)記賬
實(shí)時(shí)的交易全部是insert賬務(wù)明細(xì)(insert的開銷很小技扼,能夠支持高并發(fā)伍玖。如果基于分布式部署,insert的并發(fā)容量理論上可以無(wú)限大)剿吻,然后定時(shí)(比如每半個(gè)小時(shí))將之前半個(gè)小時(shí)內(nèi)的賬務(wù)明細(xì)sum出一個(gè)結(jié)算總金額窍箍,一筆入賬結(jié)算到指定賬戶。這個(gè)方案的缺點(diǎn)就是:交易不能實(shí)時(shí)入賬丽旅,其實(shí)如果控制好定時(shí)匯總?cè)胭~的頻度椰棘,比如分鐘級(jí),用戶也是可以接受的榄笙。這種方式對(duì)收單類業(yè)務(wù)(賬戶加錢)非常實(shí)用邪狞,但是對(duì)支出類業(yè)務(wù)(賬戶減錢)類來說,有賬戶透支地風(fēng)險(xiǎn)办斑。
3.緩沖入賬
將實(shí)時(shí)同步的記賬請(qǐng)求進(jìn)行異步化外恕,以達(dá)到記賬實(shí)時(shí)性和系統(tǒng)穩(wěn)定性之間平衡的記賬手段杆逗,這就是”削峰填谷“乡翅。詳細(xì)地講,假如賬務(wù)系統(tǒng)對(duì)同一個(gè)賬戶的處理閾值為100筆/s罪郊,24小時(shí)不間斷服務(wù)(一天能處理86400000筆)蠕蚜。當(dāng)業(yè)務(wù)高峰期來臨的時(shí)候,熱點(diǎn)賬務(wù)的請(qǐng)求數(shù)會(huì)達(dá)到200筆/s悔橄。當(dāng)賬戶的交易低于100筆/秒的時(shí)候靶累,賬務(wù)系統(tǒng)幾乎還是實(shí)時(shí)地處理了記賬請(qǐng)求,而當(dāng)交易大于100筆/秒的時(shí)候癣疟,賬務(wù)系統(tǒng)先返回結(jié)果挣柬,把賬務(wù)處理丟到可靠的處理隊(duì)列中,等并發(fā)量不大的時(shí)候慢慢消化睛挚,對(duì)用戶來說感受到的體驗(yàn)還是很快就記賬成功了邪蛔。 這個(gè)方案是有個(gè)前提是:熱點(diǎn)賬戶在某幾個(gè)高峰時(shí)間點(diǎn)需要緩沖記賬來削峰填谷,并且能在日間填完扎狱。一旦賬戶的日間交易量暴增侧到,導(dǎo)致日間隊(duì)列根本來不及消化勃教,整個(gè)隊(duì)列越來越長(zhǎng),那就不存在谷可以填匠抗,這時(shí)候肯定會(huì)帶來用戶大量的投訴故源。另外這種方案對(duì)支出類業(yè)務(wù)(賬戶減錢)來講,也會(huì)有賬戶透支地風(fēng)險(xiǎn)
4.子賬戶拆分
具體來講就是創(chuàng)建與熱點(diǎn)賬戶對(duì)應(yīng)的多個(gè)影子賬戶汞贸,所述影子賬戶與所述賬戶的數(shù)據(jù)結(jié)構(gòu)相同绳军,將所述影子賬戶設(shè)置為隱藏,并將所述賬戶的余額分散至各個(gè)影子賬戶删铃。當(dāng)賬務(wù)系統(tǒng)接收到賬務(wù)請(qǐng)求的時(shí)候顷蟆,通過前置進(jìn)行hash分配(具體的hash函數(shù)會(huì)有更多方案)選擇影子賬戶進(jìn)行記賬诫隅,這樣就將原來對(duì)一個(gè)賬戶的請(qǐng)求分散到多個(gè)影子賬戶中帐偎,分散了賬務(wù)熱點(diǎn)逐纬。 這個(gè)方案也有缺點(diǎn):通過算法選擇的影子賬戶扣款,影子賬戶的余額可能是不足的豁生,但賬戶的總余額是夠的,這樣可能影響賬務(wù)處理的成功率谴蔑。
5.內(nèi)存數(shù)據(jù)庫(kù)+緩存入賬
提高單臺(tái)數(shù)據(jù)庫(kù)服務(wù)器處理能力(I/O,CPU,memory)或者選取內(nèi)存數(shù)據(jù)庫(kù)實(shí)時(shí)地處理記賬請(qǐng)求蒂窒,然后異步地存儲(chǔ)到可靠數(shù)據(jù)庫(kù)上停士。
6.升級(jí)服務(wù)硬件,對(duì)CPU內(nèi)存等進(jìn)行升級(jí)
3:幾種方案的對(duì)比
1:并發(fā)度控制
對(duì)單個(gè)賬戶并發(fā)操作進(jìn)行限流降級(jí)控制,使得系統(tǒng)健康的完成入賬出賬操作拉一,但是在并發(fā)很高的情況下還是會(huì)殺死很多正常的沖扣功能良蛮,會(huì)極大的提高沖扣的失敗率屡贺,所以對(duì)我們賬務(wù)系統(tǒng)來說不是允許的究抓。
2:匯總明細(xì)入賬
對(duì)賬戶的沖扣操作已流水的形式記錄下來,通過定時(shí)job來將出入賬流水更新到業(yè)務(wù)表中袭灯。這種做法對(duì)于頻繁的入賬來說性能提高明顯刺下,但是因?yàn)闆]有對(duì)總金額進(jìn)行校驗(yàn),對(duì)支出類業(yè)務(wù)(賬戶減錢)類來說稽荧,有賬戶透支地風(fēng)險(xiǎn)橘茉。并且對(duì)于金額的校驗(yàn)需要通過流水?dāng)?shù)據(jù)和當(dāng)前可用余額來判定,有并發(fā)問題姨丈,計(jì)算很難準(zhǔn)確捺癞。并且我們實(shí)際線上業(yè)務(wù)是【頻繁出賬,低頻入賬】构挤,所以此辦法不可取髓介。
3:緩沖入賬
需要?jiǎng)討B(tài)判斷流量低峰高峰,維護(hù)請(qǐng)求隊(duì)列筋现,有賬戶透支地風(fēng)險(xiǎn)唐础,并且異步請(qǐng)求中結(jié)果不可控。
4:子賬戶拆分
子賬戶拆分方案中對(duì)于子賬戶的扣款進(jìn)行負(fù)載矾飞,可以滿足對(duì)同一賬戶的高頻訪問負(fù)載到其子賬戶上一膨,極大滿足了并發(fā)的需求,子賬戶的余額可能是不足的洒沦,但賬戶的總余額是夠的豹绪,這樣可能影響賬務(wù)處理的成功率,并且處理對(duì)子賬戶的扣款和入賬來說需要做到金額相對(duì)平均比較復(fù)雜申眼,對(duì)記錄賬戶期初余額期末余額處理涉及到并發(fā)瞒津,相對(duì)復(fù)雜。
5:增加硬件處理能力CPU.內(nèi)存等
備選方案,無(wú)法從根本上解決單點(diǎn)賬戶的并發(fā)壓力括尸。
6:內(nèi)存數(shù)據(jù)庫(kù)實(shí)時(shí)地處理記賬,異步入庫(kù)
使用redis做數(shù)據(jù)前置處理巷蚪,將數(shù)據(jù)庫(kù)中的熱點(diǎn)賬戶金額初始同步到redis中,然后將操作記錄流水濒翻,通過job定時(shí)任務(wù)刷新流水到業(yè)務(wù)表屁柏。這樣將db和緩存分開極大的加大了并發(fā)性能啦膜,但是卻衍生出來一個(gè)問題如下
假設(shè)redis初始金額為100,
(1) 當(dāng)線程1對(duì)redis賬戶金額進(jìn)行原子減操作時(shí)淌喻,剩余金額40,并記錄流水表等待異步入賬
(2) 當(dāng)線程2對(duì)redis賬戶金額進(jìn)行原子減操作時(shí)僧家,剩余金額-20,此時(shí)金額已經(jīng)為負(fù),按照業(yè)務(wù)要求金額不能為負(fù)所以必須要做反向操作
(3) 當(dāng)線程2還沒有對(duì)redis余額進(jìn)行反向操作維護(hù)的時(shí)候又出現(xiàn)線程3進(jìn)行充值操作裸删,此時(shí)金額又變成-20+100=80啸臀,已經(jīng)出現(xiàn)金額混亂,對(duì)業(yè)務(wù)要求的期初余額期末余額無(wú)法準(zhǔn)確的滿足烁落,所以對(duì)redis的金額進(jìn)行同時(shí)沖扣會(huì)帶來余額的并發(fā)問題乘粒。
但是對(duì)緩存進(jìn)行操作和延遲批量流水入賬可以極大的滿足我們對(duì)性能的需求,所以在【2.匯總明細(xì)記賬】和【5.內(nèi)存數(shù)據(jù)庫(kù)+緩存入賬】的基礎(chǔ)上進(jìn)行改良來滿足對(duì)我們的業(yè)務(wù)需求
4:詳細(xì)方案設(shè)計(jì)
方案設(shè)計(jì)前提:
(1):【對(duì)賬戶的余額的更新】:準(zhǔn)確的更新賬戶余額伤塌,不允許出現(xiàn)多扣灯萍,少扣等情況。
(2):【對(duì)賬戶操作記錄的更新】:準(zhǔn)確的記錄賬戶流水表中期初余額每聪,期末余額旦棉,操作金額等情況,不允許出現(xiàn)任何的金額錯(cuò)誤發(fā)生药薯。
前期準(zhǔn)備:
(1) :新增延遲入賬【流水表】绑洛,新增入賬,出賬數(shù)據(jù)先入【流水表】,通過定時(shí)任務(wù)將【流水表】入賬和出賬數(shù)據(jù)同步到業(yè)務(wù)數(shù)據(jù)表中童本,并且負(fù)責(zé)新增入賬數(shù)據(jù)的緩存同步工作真屯。
下面的方案會(huì)對(duì)此表統(tǒng)一稱為【流水表】
(2) :新增【redis】數(shù)據(jù)結(jié)構(gòu)【SortedSet(有序集合)】 key為【hotspot_account】
下面會(huì)對(duì)這個(gè)數(shù)據(jù)集合稱為【緩存操作記錄】
其中score為當(dāng)前賬戶操作時(shí)間【新覆蓋舊】,member為出入賬的賬戶ID。key【hotspot_account】,所有賬戶的入賬出賬操作需要記錄到hotspot_account中穷娱,主要是提供給【圖1中定時(shí)任務(wù)】獲取所有賬戶流水ID绑蔫。
(3)新增【redis】數(shù)據(jù)結(jié)構(gòu)【SortedSet(有序集合)】 key為【hotspot_account_currentbalance】
下面會(huì)對(duì)這個(gè)數(shù)據(jù)集合稱為【緩存余額】
其中:
score為當(dāng)前賬戶可用余額,【熱點(diǎn)賬戶新操作流程之前需要將數(shù)據(jù)庫(kù)中熱點(diǎn)賬戶的數(shù)據(jù)同步到hotspot_account_currentbalance中】
member為賬戶ID
到此泵额,前期準(zhǔn)備工作已經(jīng)全部結(jié)束配深。
當(dāng)賬戶金額充值新增時(shí):
1:記錄redis操作記錄【hotspot_account】
如圖所示紅色數(shù)據(jù)部分,當(dāng)賬戶110000056666660010入賬時(shí),插入或更新數(shù)據(jù)嫁盲,member=110000056666660010,score為當(dāng)前時(shí)間戳(秒)篓叶。
ps:操作指令【ZINCRBY key increment member】,當(dāng) key 不存在羞秤,或 member 不是 key 的成員時(shí)缸托, ZINCRBY key increment member 等同于 ZADD key increment member 。
2:新增【流水表】锥腻,設(shè)置入賬狀態(tài)為未入賬
當(dāng)賬戶金額扣減時(shí):
1:同金額充值相同首先記錄redis操作記錄【hotspot_account】嗦董。
2:直接對(duì)緩存hotspot_account_currentbalance對(duì)應(yīng)的金額進(jìn)行扣減母谎。
3:定時(shí)任務(wù)
定時(shí)任務(wù)的作用是將流水表的數(shù)據(jù)更新到【賬戶表】瘦黑,和【流水明細(xì)表】,并且設(shè)置【流水表中】數(shù)據(jù)已入賬,同時(shí)要將新入賬數(shù)據(jù)流水到更新【hotspot_account_currentbalance】中的可用賬戶余額幸斥,讓扣減操作得以繼續(xù)進(jìn)行匹摇。以下操作流程:
修改于2019年10月24日:
在第15步的時(shí)候目前版本在極端情況下會(huì)出現(xiàn)一個(gè)問題,當(dāng)定時(shí)任務(wù)發(fā)現(xiàn)了當(dāng)前緩存余額<0時(shí)甲葬,會(huì)再?gòu)牧魉碇邪褦?shù)據(jù)重新查詢一遍入賬廊勃,這里有一個(gè)小小的問題,在查詢過程中可能會(huì)有部分線程沒有入賬到數(shù)據(jù)庫(kù)经窖,這樣會(huì)出現(xiàn)超扣的問題坡垫,也就是說線程1在
扣減redis的時(shí)候成功了,但是沒有insert到流水表画侣,這個(gè)時(shí)候又有一個(gè)線程2扣減redis的線程扣負(fù)了冰悠,這個(gè)時(shí)候定時(shí)任務(wù)發(fā)現(xiàn)余額為負(fù),不應(yīng)該直接同步余額正確的做法是加鎖等待扣減操作流程執(zhí)行完畢配乱,由于加鎖的復(fù)雜性溉卓,所以這里采用一個(gè)簡(jiǎn)單的辦法,由于扣減redis和mysql操作基本都是瞬時(shí)的搬泥,所以直接sleep 5 s即可桑寨,這個(gè)時(shí)間足夠發(fā)生full gc等一些其他未知因素的耗時(shí)了,當(dāng)然感興趣的小伙伴也可以加鎖去自己拓展實(shí)現(xiàn)忿檩,會(huì)有一定性能上的影響尉尾,最后感謝群里小伙伴@Token指出的問題。
加筆者微信mingyuan_2018燥透,或者掃碼加群代赁,即可獲取完整版pdf資料,以及更多技術(shù)方案設(shè)計(jì)+代碼示例+ 面試資料+ 享受美團(tuán)兽掰,阿里芭碍,頭條內(nèi)推福利 ↓↓↓
注意:想去其他互聯(lián)網(wǎng)大廠的勿擾,目前只有美團(tuán)孽尽,阿里窖壕,頭條的內(nèi)推通道