天天看點

萬級TPS億級流水-中台賬戶系統架構設計

萬級TPS億級流水-中台賬戶系統架構設計

我們需要給所有前台業務提供統一的賬戶系統,用來支撐所有前台産品線的使用者資産管理,統一提供支援大并發萬級TPS、億級流水、資料強一緻、風控安全、日切對賬、财務核算、審計等能力,在萬級TPS下保證絕對的資料準确性和資料溯源能力。

注:資金類系統隻有合格和不合格,哪怕資料出現隻有0.01分的差錯也是不合格的,局部資料不準也就意味着全局資料都不可信。

萬級TPS億級流水-中台賬戶系統架構設計

标簽:高并發 萬級TPS 億級流水 賬戶系統

  • 背景
  • 業務模型
  • 應用層設計
  • 資料層設計
  • 日切對賬

本文隻分享系統的核心模型部分的設計,其他正常類的(如壓測驗收、系統保護政策-限流、降級、熔斷等)設計就不做多介紹,如果對其他方面有興趣歡迎進一步交流。

基本賬戶管理: 根據交易的不同主體,可以分為

個人賬戶

機構賬戶

賬戶餘額

在使用上沒有任何限制,很純粹的賬戶存儲、轉賬管理,可以滿足90%業務場景。

子賬戶功能: 一個使用者可以開通多個子賬戶,根據餘額屬性不同可以分為基本賬戶、過期賬戶,根據币種不同可以分為人民币賬戶、虛拟币賬戶,根據業務形态不同可以自定義。

(不同賬戶的特定功能是通過賬戶上的

賬戶屬性

來區分實作。)

過期賬戶管理: 該賬戶中的餘額是會随着

進賬流水

到期自動過期。

如:在某平台充值1000元送300元,其中300元是有過期時間的,但是1000元是沒有時間限制的。這裡的1000元存在你的基本賬戶中,300元存在你的過期賬戶中。

注:過期賬戶的每一筆入賬流水都會有一個到期時間。系統根據交易流水的到期時間,自動核銷使用者過期賬戶中的餘額,記為平台的确認收入。

賬戶組合使用:支援多賬戶組合使用,根據配置的優先扣減順序進行扣減餘額。比如:在

基本賬戶

過期賬戶

(充值賬戶)中扣錢一般的順序是優先扣減過期賬戶的餘額。

根據上述業務模型,賬戶系統是一個典型的 資料密集型系統 ,業務層的邏輯不複雜。整個系統的設計關鍵點在于如何平衡大并發TPS和資料一緻性。

熱點賬戶:前台直播類業務存在熱點賬戶問題,每到各種活動賽事的時候會存在 90%DAU 給少數幾個頭部主播打賞的場景。

DB就會有熱點行問題,由于 行鎖 關系并發一大肯定大量逾時、RT突增 、 DB活躍線程 增加等一系列問題,最終DB會被拖挂。

賬戶類系統有一個特點,原賬戶的扣減可以實時處理,目标賬戶可以異步處理,我們可以将轉賬動作拆解為兩個階段進行異步化。(可以參考銀行轉賬業務。)

比如:A給B轉賬100元,原賬戶A的100元餘額扣減可以同步處理,B賬戶的100增加可以異步處理。這樣哪怕10w人給主播打賞,這10w人的賬戶都是分散的,而主播的餘額增加則是異步處理的。

賬戶轉賬扣減A賬戶餘額,記錄A賬戶出賬流水,記錄B賬戶入賬流水,這三個動作可以在一個

DBTransaction

中處理,可以保證源賬戶進出帳一緻性。目标賬戶B的入賬可以異步處理,為了保證萬無一失且滿足一定的實時性,需要兩步結合,程式裡通過

MQ

走異步入賬,同時增加DB的兜底

JOB

定時掃描 入賬流水記錄 為

未到賬

的流水進行入賬。

萬級TPS億級流水-中台賬戶系統架構設計
我們通過異步化緩解熱點行處理,但是如果 收款方 強烈要求收款必須在一定的時間内完成,我們還是需要進一步處理,後面會講到。

過期賬戶: 通常過期賬戶用來管理贈送類賬戶,這類賬戶有一定的時效性,使用者在使用上也是優先扣減此類賬戶餘額。

這類使用需求其實覆寫面不大,真正使用者賬戶餘額不使用等着被系統過期的很少,畢竟這是一個很傻的行為。

過期賬戶的兩種核銷情況:第一種是使用者使用過期賬戶時的核銷。第二種是某個過期流水到了過期時間,系統自動核銷記為平台的确認收入。

過期賬戶核銷邏輯:使用者充值1000元到

基本賬戶

,平台贈送300元到

贈送賬戶

。此時,

基本賬戶

記錄

進賬流水

+1000元,

贈送賬戶

進賬流水

+300元并且該筆流水的

過期時間

為 2020-12-29 23:59:59 (過期時間由前台業務方設定) 。

系統自動核銷:如果使用者不在此時間之前用完就會被系統自動劃進平台的收入,

贈送賬戶

餘額扣減-300元。
使用者使用核銷:如果使用者在

過期時間

前陸續在使用

贈送賬戶

,比如使用100元,那麼我們需要核銷原本進賬的300元的那筆流水,減少-150元。

也就是說,該筆過期流水已經核銷掉150元,帶過期核銷150元,到期後隻要核銷150元即可,而不是300元。

過期賬戶每次使用均産生

待核銷

負向流水,系統自動核銷前必須保證沒有任何負向流水記錄才可以去扣減贈送賬戶餘額。

萬級TPS億級流水-中台賬戶系統架構設計

考慮到極端情況下,剛好過期

JOB

在進行自動過期核銷,使用者又在此時使用過期賬戶,這點需要注意下。可以簡單通過加

DB-X

鎖解決,這個場景其實非常稀少。

在應用層設計的時候,我們通過異步化方式來繞開熱點問題。

同樣我們在設計資料層的時候也要考慮單次操作DB的性能,比如控制事務的大小,事務跨網絡的次數等問題。當然還包括金額存儲的精度問題,精度問題處理不好也會影響性能。

浮點數問題: 如果我們用浮點數近似值來存儲金額,那麼就一定會有偏差,随着金額越大時間越長偏差就會越大。比較好的方式是通過整型來存儲,通過放大

金額比例

來達到不同的業務場景下對

金額比率

的要求。

正常的1.12元,存儲比率是1=100元,那麼表裡的存儲值就是112,不同的貨币比例都可以自由縮放,永遠都可以保持最準确的精度。

分庫分表+讀寫分離: 根據業務特點和未來增量規劃,将DB分為16個邏輯庫,前期使用2個實體庫承載。16個邏輯庫,按照每次2倍擴容,最大擴容上限是16個實體庫。單執行個體的配置 8c 32g 2t 8000conn 9000iops 。

按照單次TPS-rt 1ms計算,TPS 1w 需求,每台承載5k TPS,單庫的活躍線程大概在8-10個(考慮網絡延遲)。

最後到達瓶頸的都是iops,因為隻要rt足夠短,最終壓力都會在IO上。

分庫按照uid分為16個庫,賬戶表不分表預設16張。每張表按照 1kw*16=1.6 億個賬戶。

單表能存儲多少要綜合考慮,比如查詢類型,單次查詢的RT,冷熱資料占比( innodb_buffer_pool 使用率)、是否充分發揮了索引,索引是否達到3星級别,

索引片

中沒有經常變更的字段等。

賬戶流水表按照日期分表365張,流水資料會随着時間推移逐漸變成冷資料,定期歸檔冷資料。(這裡約定了,流水查詢隻能按照uid+日期查詢。如果營運類的需求,要橫跨分片key擷取,走OLAP方案 clickhouse、hive等)

分庫分表采用阿裡雲分布式資料庫産品DRDS,1個主庫叢集+2個讀庫叢集(讀庫做了讀負載均衡,可以按需擴容)。

萬級TPS億級流水-中台賬戶系統架構設計
讀負載均衡器:https://github.com/Plen-wang/read-loadbalance

既然用了DRDS分布式資料庫産品,那麼在查詢上需要充分考慮

分片鍵

的限制,如果存儲和查詢出現分片鍵沖突問題就需要我們手動計算分片路由,直接通路實體節點。

通路實體節點需要借助DRDS專用

SQL注釋

子句來完成。

先通過

show node

檢視實體DB ID、

show topology from logic_table_name

檢視實體表ID,然後在SQL帶上特定的

注釋子句

SELECT /*+TDDL:scan('logic_table_name', real_table=("real_table_name"),node='real_db_node_id')*/ 
count(1) FROM logic_table_name ;
           

賬戶更新: 對賬戶更新都有一個前提就是賬戶已經開通,但是我們為了最大化賬戶系統在使用上的便利性,讓前台業務方不需要做初始化動作,由賬戶系統

惰性初始化

,比如發現賬戶不存在就自動初始化賬戶資料。

但是我們怎麼知道賬戶不存在,不可能每次都去查詢一次或者根據執行傳回錯誤判斷。而且 update 語句是區分不了錯誤的 賬戶不存在 還是 餘額不足 或者其他原因。

那麼如何巧妙的解決這個問題,隻要一次DB往返。

我們可以使用 Mysql

INSERT INTO ... ON DUPLICATE KEY UPDATE ...

子句,但是該子句有一個限制就是不支援

where

子句。

-- cut_version 樂觀鎖、account_property 賬戶屬性
insert into tb_account(uid,balance,cut_version,account_property) values("%s",%d,%d,%d) ON DUPLICATE KEY UPDATE balance = balance + %d,cut_version = cut_version+1
           

其實不完全推薦使用這個方法,因為這個方法也有弊端就是将來 where 子句無法使用,還有一個辦法就是合并 賬戶查詢 和 插入 為一條 sql 送出。

DB操作本身rt可能很短,但是如果跨網絡那麼事務的延遲會帶來DB的串行化增加,降低并發度,整體應用 rt就會增加。是以一個原則就是盡量不要跨網絡開事務,合并sql做一次事務送出,最短的事務周期,減少跨網絡的事務操作,如果我們将單次事務網絡互動減少2-3次,性能的提高可能會增加2-3倍,同樣由于網絡的不穩定抖動丢包對 999rt 線的影響也會減少2-3倍。

平衡好目前系統是

業務密集型

還是

資料密集型

判斷目前系統是否有很強的業務層邏輯,是否要運用

DDD

RUP

等強模型的工程方法。畢竟

強模型

高性能

在落地的時候有些方面是沖突的,需要進一步借助

CRQS

GRASP

等工程方法來解決。

單行熱點問題: 單行的TPS都是串行的,事務rt越短TPS就越高,按照1ms計算,差不多TPS就是1000。一般隻有

機構賬戶

類型才會有這個需求。

我們可以将單行變成多行,增加行的并行度,加大賬戶操作的并發度。(這個方案要評估好寫入和查詢兩端需求)

id uid balance slot
1 10101010 1000
2 2000
3 3000
4 400
5 300
6 200
7
8
9
10
insert into tb_account (uid,balance,slot)
values(10101010, 1000, round(rand()*9)+1) 
on  duplicate key update balance=balance+values(balance)
           

這裡的 10slot*單個slot 1000TPS,理論上可以跑到1w,如果機構賬戶資料量很大,可以擴充

slot

個數。

賬戶的總餘額通過

sum()

彙總,如果業務場景中有餘額的頻繁sum()操作,可以通過增加餘額中間表,定期

insert into tb_account_total select sum(balance) total_balance from tb_account group by uid

通常機構賬戶的結算是有周期的(T+7、T+30等),而且基本是沒有并發,是以在賬戶餘額扣減方面就可以輕松處理。

有兩種實作方案:

第一種,賬戶餘額允許單個slot為負數,但是總的sum()是正數。通過子查詢來對餘額進行檢查。

insert into tb_account (uid, balance, slot)
select uid,-1000 as balance,round(rand() *9+ 1)
from(
    select uid, sum(balance) as ss
    from tb_account
    where uid= 10101010
    group by uid having ss>= 1000 for update) as tmp
on duplicate key update balance= balance+ values(balance)
           

第二種,如果條件允許可以借助

使用者自定義

變量來在DB上完成餘額累計掃描,将可以扣減的slot的主鍵id傳回給程式,但是隻需要一次DB互動就可以擷取出可以扣減的賬戶solt,然後分别開始對slot賬戶進行扣減。

set @f:=0;
select * from tb_account where id in(select id from (select id, @f:=@f+balance from tb_account where @f<1000 order by id) as t);
           

第二種方案在預設的mysql資料庫上都是支援的,但是有些資料庫雲産品不支援,阿裡雲rds是不支援的。

賬戶系統有一個基本的需求,就是每天餘額鏡像,簡單講就是餘額在每天的快照,用來做T+1對賬。

不管财務還是每季度的審計都會需要,最重要的是我們自己也需要對賬戶資料做摸底對賬。

由于每天産生上億的流水,這需要在大資料平台中完成。

日切對賬:

昨天賬戶餘額

-

前天賬戶餘額

=

昨天的流水

前天的流水

比如,昨天的賬戶餘額是5000w,前台的賬戶餘額是4500w,內插補點就是500w。同樣道理,昨天的賬戶流水是5000w,前天的賬戶流水是4500w,那麼內插補點是500w,這就是沒問題的。
賬戶不僅有增加也有減少,可能昨天賬戶餘額比前天賬戶餘額內插補點是-500w,但是流水也要是-500w才行。

由于每天會産生億級的流水,用傳統的全量抽取不現實,這類資料抽取的速度都會有延遲,而且對賬最重要的是時間點必須非常精準,才能保證餘額和流水是對得上的。

要不然會出現HDFS的分區是2020-06-10号,但是該分區裡有2020-06-11的資料,就是因為拉取的時候會延遲到第二天。這個問題也可以通過增加拉取sql的條件限制來解決這個問題,但是無法做到0點瞬間鏡像全部賬戶。

解決方案: 全量餘額+binlog增量更新

1.賬戶表,先做一次全量同步。

2.DB的所有變更通過binlog(預設row複制)進到數倉。(因為 binlog 是基于發生時間的,是以無所謂我們是不是在0點去計算鏡像)

3.T+1跑JOB的時候,擷取前一天的賬戶餘額,然後通過 binlog 來覆寫前天與昨天的交集部分。

由于數倉的 binlog 資料都是增量的,是以要想取到正确的全量資料需要用到一定的技巧。

select app_id,sub_type,sum(amount) records_amount from (
      select *,row_number()over(partition by id order by updated_at) as rn
      from hive_db_table
      where dt='${YESTERDAY}'
  ) t where t.rn=1
       group by t.sub_type,t.app_id
           

使用 hive 開窗函數

row_number()over()

對同樣的id進行分組,然後擷取最新的一條資料就是賬戶在T的最後的值。

作者:王清培(趣頭條 Tech Leader)

繼續閱讀