高頻交易支付架構並不復雜


 

寫在前面

支付系統是整個交易系統中相當核心的一部分功能,以我們的交易中台為例,通過領域方式的拆分,支付架構隸屬於訂單團隊,在整個用戶下單之后進行支付,支付之后成單進入交易履約流程。

支付系統由於本身和金融相關,不像其他高頻系統面對海量請求可以大量使用緩存,異步mq等方式解決三高問題。支付系統對數據一致性要求更高,所以對於其架構設計原則還是有自己特點的。

分庫分表

構建一個支撐每秒十萬只讀系統並不復雜,無非是通過一致性哈希擴展緩存節點,水平擴展web服務器等。每秒鍾數十萬數據更新操作,在任何數據庫上都是不可能的任務,首先需要對訂單表進行分庫分表。

在進行數據庫操作時,一般會用ID(UID)字段,所以選擇以UID進行分庫分表。

分庫策略我們選擇了“二叉樹分庫”,所謂“二叉樹分庫”指:在進行數據庫擴容時,以2倍數進行擴容。比如:1台擴容2台,2台擴容4台,以此類推。最后把Order庫分了8個庫中,每個庫10個表。

根據uid計算數據庫編號:

分庫信息 = (uid / 10) % 8 + 1 
根據uid計算表編號: 
表編號 = uid %10

訂單ID

訂單系統的ID必須具有全局唯一的特征,簡單的方式是利用數據庫的序列,每操作一次就能獲得一個全局唯一的自增ID,如果支持每秒10w訂單,那每秒至少需要生成10w訂單ID,通過數據庫自增ID顯然無法完成上述請求。所以通過內存計算獲取全局唯一的訂單ID。

JAVA領域著名的唯一ID應該是UUID了,不過UUID太長且包含字母,不適合做訂單ID。

通過反復比較篩選,借鑒Twitter的算法實現全局唯一ID。

三部分組成:

  • 時間戳

    時間戳的粒度是毫秒級,生成訂單ID時,使用System.currentTimerMillis()作為時間戳。

  • 機器號

    每個訂單服務器都被分配一個唯一的編號,生成訂單ID時,直接使用該唯一編號作為機器即可。

  • 自增序號

    當同一服務器的同一號碼中有多個生成訂單ID的請求時,會在當前毫秒下自增此序號,下一個毫秒此序號繼續同0開始。如同一服務器同一毫秒生成3個訂單ID請求,這3個訂單ID的自增序號分別是0,1,2。

最終訂單結構:

分庫分表信息 + 時間戳 + 機器號 + 自增序號

還是按照第一部分根據uid計算數據庫編號和表編號的算法,當uid=9527時,分庫信息=1,分表信息=7,將他們進行組合,兩位的分庫分表信息即為”17”。

最終一致性

我們通過對order表uid維度的分庫分表,實現了order表的超高並發寫入與更新,通過uid和訂單ID查詢訂單信息。

上面方案雖然簡單,但是保持兩個order表機器的數據一致是很麻煩的事情。

兩個表集群顯然是在不同的數據庫集群中,如果寫入與更新中引入強一致性的分布式事務,這無疑會大大降低系統效率,增長服務響應時間,這是我們所不能接受的,所以引入了消息隊列進行異步數據同步,為了實現數據的最終一致性。

當然消息隊列的各種異常會造成數據不一致,所以我們又引入了實時服務監控,實時計算兩個集群的數據差異,並進行一致性同步。

數據庫高可用

所謂數據庫高可用指的是:

當數據庫由於各種原因出現問題時,能實時或快速的恢復數據庫並修補數據。

從整體集群角度看,就像沒有出任何問題一樣,需要注意的是,這里的恢復數據庫服務並不一定是指修復原有數據庫,也包括將服務切換到另外備用的數據庫。

數據庫高可用的主要工作是數據恢復月數據修補,一般我們完成這兩項工作的時間長短,作為衡量高可用好壞的標准。

 

我們認為,數據庫運維應該和項目組分開,當數據庫出現問題時,應由DBA實現統一恢復,不需要項目組操作服務,這樣便於做到自動化,縮短服務恢復時間。

如上圖所示,web服務器將不再直接連接從庫DB2和DB3,而是連接LVS負載均衡,由LVS連接從庫。

這樣做的好處是LVS能自動感知從庫是否可用,從庫DB2宕機后,LVS將不會把讀數據請求再發向DB2。

同時DBA需要增減從庫節點時,只需獨立操作LVS即可,不再需要項目組更新配置文件,重啟服務器來配合。

再來看主庫高可用結構圖:

 

如上圖所示,web服務器將不再直接連接主庫DB1,而是連接KeepAlive虛擬出的一個虛擬ip,再將此虛擬ip映射到主庫DB1上,同時添加DB_bak從庫,實時同步DB1中的數據。

正常情況下web還是在DB1中讀寫數據,當DB1宕機后,腳本會自動將DB_bak設置成主庫,並將虛擬ip映射到DB_bak上,web服務將使用健康的DB_bak作為主庫進行讀寫訪問。

這樣只需幾秒的時間,就能完成主數據庫服務恢復。

組合上面的結構,得到主從高可用結構圖:

數據庫高可用還包含數據修補,由於我們在操作核心數據時,都是先記錄日志再執行更新,加上實現了近乎實時的快速恢復數據庫服務,所以修補的數據量都不大,一個簡單的恢復腳本就能快速完成數據修復。

數據分級

支付系統除了最核心的支付訂單表與支付流水表外,還有一些配置信息表和一些用戶相關信息表。如果所有的讀操作都在數據庫上完成,系統性能將大打折扣,所以我們引入了數據分級機制。

我們簡單的將支付系統的數據划分成了3級:

  • 第1級:訂單數據和支付流水數據;這兩塊數據對實時性和精確性要求很高,所以不添加任何緩存,讀寫操作將直接操作數據庫。

  • 第2級:用戶相關數據;這些數據和用戶相關,具有讀多寫少的特征,所以我們使用redis進行緩存。

  • 第3級:支付配置信息;這些數據和用戶無關,具有數據量小,頻繁讀,幾乎不修改的特征,所以我們使用本地內存進行緩存。

使用本地內存緩存有一個數據同步問題,因為配置信息緩存在內存中,而本地內存無法感知到配置信息在數據庫的修改,這樣會造成數據庫中數據和本地內存中數據不一致的問題。

為了解決此問題,我們開發了一個高可用的消息推送平台,當配置信息被修改時,我們可以使用推送平台,給支付系統所有的服務器推送配置文件更新消息,服務器收到消息會自動更新配置信息,並給出成功反饋。

粗細管道

舉個簡單的例子,我們目前訂單的處理能力是平均10萬下單每秒,峰值14萬下單每秒,如果同一秒鍾有100萬個下單請求進入支付系統,毫無疑問我們的整個支付系統就會崩潰,后續源源不斷的請求會讓我們的服務集群根本啟動不起來,唯一的辦法只能是切斷所有流量,重啟整個集群,再慢慢導入流量。

我們在對外的web服務器上加一層“粗細管道”,就能很好的解決上面的問題。

請看上面的結構圖,http請求在進入web集群前,會先經過一層粗細管道。入口端是粗口,我們設置最大能支持100萬請求每秒,多余的請求會被直接拋棄掉。出口端是細口,我們設置給web集群10萬請求每秒。

剩余的90萬請求會在粗細管道中排隊,等待web集群處理完老的請求后,才會有新的請求從管道中出來,給web集群處理。

這樣web集群處理的請求數每秒永遠不會超過10萬,在這個負載下,集群中的各個服務都會高校運轉,整個集群也不會因為暴增的請求而停止服務。

如何實現粗細管道?nginx商業版中已經有了支持,相關資料請搜索

nginx max_conns,需要注意的是max_conns是活躍連接數,具體設置除了需要確定最大TPS外,還需確定平均響應時間。

微信掃描二維碼,關注我的公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM