業務單表讀寫緩慢如何優化?
在前面的文章中探討了架構優化的兩種方案:冷熱分離、查詢分離。
- 冷熱分離
- 查詢分離
查詢分離其實就是利用了非關系數據庫的高性能,但是不足之處也很明顯:當主數據量越來越多,寫操作緩慢;這種問題如何破局?可見任何一種優化方案都不是最終的銀彈,只有不斷的優化演變
這篇文章就來介紹一下解決方案:分庫分表,將圍繞以下幾點介紹:
- 拆分后的存儲選型?
- 分庫分表的實現思路?
- 分庫分表的不足?
拆分后的存儲選型?
在介紹選型之前先來介紹下架構背景,筆者曾經做過電商系統的優化,該系統中包含的兩個主體:
- 用戶:數據量上千萬,每日增長10W+
- 訂單:數據量上億,每日百萬級的增長
對于如此量級的數據,單庫單表的情況下,無論是IO還是CPU都扛不住,架構上的優化是必然。
經過了多次探討嘗試,最終選擇了分庫分表。
說到分庫分表首先想到的就是存儲選型,關于持久層的選型主流的無非有如下幾種:
- 關系型數據庫:MySQL、Oracle.........
- NoSQL:MongoDB、ES......
- NewSQL:TiDB........
1. 關系型數據庫
關系型數據庫目前市面上主流無非三種:MySQL、Oracle、SqlServer,筆者更傾向于MySQL,也是很多新型企業在用的一種數據庫,因此本篇文章也將重點圍繞MySQL展開
在任何系統中關系型數據庫的地位都是不可或缺的,它的強約束性、事務的控制、SQL語法、鎖....這些功能可謂是久經考驗,因此在功能上 MySQL 能滿足我們所有的業務需求。
2. NoSQL
說到NoSQL,第一個想到就是MongoDB ,它的分片功能從并發性和數據量這兩個角度已經能滿足一般大數據量的需求,但是仍然需要考慮如下幾點:
- 約束性:MongoDB 不是關系型數據庫而是文檔型數據庫,它的每一行記錄都是一個結構靈活可變的 JSON,比如存儲非常重要的訂單數據時,我們就不能使用 MongoDB,因為訂單數據必須使用強約束的關系型數據庫進行存儲。
- 業務功能考量:事務的控制、SQL語法、鎖以及各種千奇百怪的SQL在已有的架構上都曾久經考驗,但是MongoDB在這些功能需要上并不能滿足
- 業務改造考量:未拆分前使用關系型數據庫,使用NoSQL之后對于SQL的改造比較麻煩,項目周期更長
3. NewSQL
NewSQL目前比較主流則是TiDB,該技術比較新,雖然能夠滿足大數據量的存儲,但是在選擇上還是需要做些考量:
熟悉程度考量:如果你所在公司架構組對于NewSQL比較數據或者已經有在使用,則可以選擇
穩定性考量:關系型數據畢竟是久經考驗,在穩定性方面肯定是比較好,但是NewSQL的穩定性卻無法去考量,建議初期階段可以將一些不太重要的數據使用NewSQL存儲
基于MySQL的分庫分表
什么是分表分庫?分表是將一份大的表數據拆分存放至多個結構一樣的拆分表;分庫就是將一個大的數據庫拆分成多個結構一樣的小庫。
前面介紹的三種拆分存儲技術,在我以往的項目中我都沒使用過,而是選擇了基于 MySQL 的分表分庫,主要是有一個重要考量:分表分庫對于第三方依賴較少,業務邏輯靈活可控,它本身并不需要非常復雜的底層處理,也不需要重新做數據庫,只是根據不同邏輯使用不同 SQL 語句和數據源而已。
目前市面上主流的分庫分表分為兩種模式:Proxy模式、Client模式
Proxy模式屬于業務無侵入型,直接代理數據庫,對于開發者一切都是無感知的,SQL 組合、數據庫路由、執行結果合并等功能全部存放在一個代理服務中,比如MyCat、ShardingSphere都對Proxy模式提供了支持
Client模式屬于業務侵入型,將分庫分表的邏輯放在客戶端,客戶端需要引入一個jar,比如Sharding-JDBC,架構圖如下:
市面上對于分庫分表中間件如下:
兩種模式的優缺點也很明顯:
- Proxy模式:資源解耦,業務無侵入;缺點則是運維成本相對較高
- Client模式:代碼靈活控制,運維成本低;缺點則是語言限制,升級不方便
分庫分表的實現思路
在落實分表分庫解決方案時,我們需要考慮 5 個要點。
1. 分片鍵如何選擇?
針對訂單這個業務,其中涉及到以下幾個主要的字段:
- user_id:用戶id
- order_id:訂單id
- order_time:下單時間
- store_id:店鋪id
經過考量,最終選擇了user_id作為ShardingKey,為什么呢?
選擇user_id作為ShardingKey需要結合業務場景,訂單系統中常見的業務:
- C端用戶需要查詢所有的訂單(user_id)
- 后臺需要根據城市查詢所有訂單(user_city_id)
- B端商家需要統計自己店鋪的下單量(store_id)
以上三種業務場景,判斷下優先級,C端用戶肯定是需要優先滿足,因此使用user_id作為ShardingKey
這樣在查詢時需要將user_id傳遞過來才能定位到指定庫、表
選擇字段作為分片鍵時,我們一般需要考慮三點要求:數據盡量均勻分布在不同表或庫、跨庫查詢操作盡可能少、這個字段的值不會變(這點尤為重要)。
2. 分片的策略是什么?
選擇user_id作為ShardingKey之后,需要考慮使用分片策略了,主要分為如下三種
- 范圍分片
假如user_id是自增的數字,則可以根據user_id范圍進行分片,每100萬份分為一個庫,每10萬份分為一個表,此時單個庫中將分為10張表,如下表:
- Hash取模
這種方案是根據Hash值進行分片,比如Hash函數為:hash(user_id%8),這里是將user_id對8這個特定值取模,最終分為了8張表;這里一般為了方便后續擴容,建議選擇2的N次方
- 范圍分片和Hash取模混合
比如先按照范圍對user_id拆分,每100萬份分為一個庫,在對這100萬份數據進行Hash取模(hash(user_id%8))拆分成8個表
當然以上三種方案的優缺點也是非常明顯,這里不再贅述了
需要注意的是:在拆分之前,為了避免頻繁的擴容,一定要對未來5年或者10年數據增長做個判斷,預留更多的分片
3. 業務代碼如何修改
業務代碼的修改這里就不好說了,和自身的業務是強關聯。
但是,在這里我想分享一些個人觀點。近年來,分表分庫操作愈發容易,不過我們需要注意幾個要點。
我們已經習慣微服務了,對于特定表的分表分庫,其影響面只在該表所在的服務中,如果是一個單體架構的應用做分表分庫,那真是傷腦筋。
在互聯網架構中,我們基本不使用外鍵約束。
隨著查詢分離的流行,后臺系統中有很多操作需要跨庫查詢,導致系統性能非常差,這時分表分庫一般會結合查詢分離一起操作:先將所有數據在 ES 索引一份,再使用 ES 在后臺直接查詢數據。如果訂單詳情數據量很大,還有一個常見做法,即先在 ES 中存儲索引字段(作為查詢條件的字段),再將詳情數據存放在 HBase 中(這個方案我們就不展開了)。
4. 歷史數據遷移?
歷史數據的遷移非常耗時,有時遷移幾天幾夜都很正常。而在互聯網行業中,別說幾天幾夜了,就連停機幾分鐘業務都無法接受,這就要求我們給出一個無縫遷移的解決方案。
還記得講解查詢分離時,我們說過的方案嗎?我們再來回顧下,如下圖所示:
歷史數據遷移時,我們就是采用類似的方案進行歷史數據遷移,如下圖所示:
此數據遷移方案的基本思路:存量數據直接遷移,增量數據監聽 binlog,然后通過 canal 通知遷移程序搬運數據,新的數據庫擁有全量數據,且校驗通過后逐步切換流量。
數據遷移解決方案詳細的步驟如下:
- 上線 canal,通過 canal 觸發增量數據的遷移;
- 遷移數據腳本測試通過后,將老數據遷移到新的分表分庫中;
- 注意遷移增量數據與遷移老數據的時間差,確保全部數據都被遷移過去,無遺漏;
- 第二步、第三步都運行完后,新的分表分庫中已經擁有全量數據了,這時我們可以運行數據驗證的程序,確保所有數據都存放在新數據庫中;
- 到這步數據遷移就算完成了,之后就是新版本代碼上線了,至于是灰度上還是直接上,需要根據你們的實際情況決定,回滾方案也是一樣。
5. 未來的擴容方案是什么?
隨著業務的發展,如果原來的分片設計已經無法滿足日益增長的數據需求,我們就需要考慮擴容了,擴容方案主要依賴以下兩點。
- 分片策略是否可以讓新表數據的遷移源只是 1 個舊表,而不是多個舊表,這就是前面我們建議使用 2 的 N 次方分表的原因;
- 數據遷移:我們需要把舊分片的數據遷移到新的分片上,這個方案與上面提及的歷史數據遷移一樣,我們就不重復啰唆了。
分表分庫的不足
分表分庫的解決方案講完了,以上就是業界常用的一些做法,不過此方案仍然存在不足之處。
- 增量數據遷移:如何保證數據的一致性及高可用性
- 短時訂單量大爆發:分表分庫仍然扛不住時解決方案是什么?