寫在前面
支付系統是整個交易系統中相當核心的一部分功能,以我們的交易中臺為例,通過領域方式的拆分,支付架構隸屬于訂單團隊,在整個用戶下單之后進行支付,支付之后成單進入交易履約流程。
支付系統由于本身和金融相關,不像其他高頻系統面對海量請求可以大量使用緩存,異步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外,還需確定平均響應時間。
end:如果你覺得本文對你有幫助的話,記得關注點贊轉發,你的支持就是我更新動力。
如果您有不同的看法,歡迎在評論區留言與我們一起討論