數據庫優化方案中寫入數據量增加時,如何實現分庫分表?

我們知道在高併發下數據庫的一種優化方案:讀寫分離,它就是依靠主從複製的技術使得數據庫實現了數據複製爲多份,增強了抵抗大量併發讀請求的能力,提升了數據庫的查詢性能的同時,也提升了數據的安全性,當某一個數據庫節點,無論是主庫還是從庫發生故障時,我們還有其他的節點中存儲着全量的數據,保證數據不會丟失。
此時,你的電商系統的架構圖變成了下面這樣:

這時,公司 CEO 突然傳來一個好消息,運營推廣持續帶來了流量,你所設計的電商系統的訂單量突破了五千萬,訂單數據都是單表存儲的,你的壓力倍增,因爲無論是數據庫的查詢還是寫入性能都在下降,數據庫的磁盤空間也在報警。所以,你主動分析現階段自己需要考慮的問題,並尋求高效的解決方式,以便系統能正常運轉下去。你考慮的問題主要有以下幾點:

1. 系統正在持續不斷地的發展,註冊的用戶越來越多,產生的訂單越來越多,數據庫中存儲的數據也越來越多,單個表的數據量超過了千萬甚至到了億級別。這時即使你使用了索引,索引佔用的空間也隨着數據量的增長而增大,數據庫就無法緩存全量的索引信息,那麼就需要從磁盤上讀取索引數據,就會影響到查詢的性能了。那麼這時你要如何提升查詢性能呢?

2. 數據量的增加也佔據了磁盤的空間,數據庫在備份和恢復的時間變長,你如何讓數據庫系統支持如此大的數據量呢?

3. 不同模塊的數據,比如用戶數據和用戶關係數據,全都存儲在一個主庫中,一旦主庫發生故障,所有的模塊兒都會受到影響,那麼如何做到不同模塊的故障隔離呢?

4. 你已經知道了,在 4 核 8G 的雲服務器上對 MySQL5.7 做 Benchmark,大概可以支撐 500TPS 和 10000QPS,你可以看到數據庫對於寫入性能要弱於數據查詢的能力,那麼隨着系統寫入請求量的增長,數據庫系統如何來處理更高的併發寫入請求呢?

這些問題你可以歸納成,數據庫的寫入請求量大造成的性能和可用性方面的問題,要解決這些問題,你所採取的措施就是對數據進行分片,對數據進行分片,可以很好地分攤數據庫的讀寫壓力,也可以突破單機的存儲瓶頸,而常見的一種方式是對數據庫做“分庫分表”。

分庫分表是一個很常見的技術方案,你應該有所瞭解。那你會說了:“既然這個技術很普遍,而我又有所瞭解,那你爲什麼還要提及這個話題呢?”因爲以我過往的經驗來看,不少人會在“分庫分表”這裏踩坑,主要體現在:
1. 對如何使用正確的分庫分表方式一知半解,沒有明白使用場景和方法。比如,一些同學會在查詢時不使用分區鍵;
2. 分庫分表引入了一些問題後,沒有找到合適的解決方案。比如,會在查詢時使用大量連表查詢等等。 本節課,我就帶你解決這兩個問題,從常人容易踩坑的地方,跳出來。

如何對數據庫做垂直拆分
分庫分表是一種常見的將數據分片的方式,它的基本思想是依照某一種策略將數據儘量平均的分配到多個數據庫節點或者多個表中。
不同於主從複製時數據是全量地被拷貝到多個節點,分庫分表後,每個節點只保存部分的數據,這樣可以有效地減少單個數據庫節點和單個數據表中存儲的數據量,在解決了數據存儲瓶頸的同時也能有效的提升數據查詢的性能。同時,因爲數據被分配到多個數據庫節點上,那麼數據的寫入請求也從請求單一主庫變成了請求多個數據分片節點,在一定程度上也會提升併發寫入的性能。

比如,我之前做過一個直播項目,在這個項目中,需要存儲用戶在直播間中發的消息以及直播間中的系統消息,你知道這些消息量極大,有些比較火的直播間有上萬條留言是很常見的事兒,日積月累下來就積攢了幾億的數據,查詢的性能和存儲空間都扛不住了。沒辦法,就只能加班加點重構,啓動多個數據庫來分攤寫入壓力和容量的壓力,也需要將原來單庫的數據遷移到新啓動的數據庫節點上,好在最後成功完成分庫分表和數據遷移校驗工作,不過也着實花費了不少的時間和精力。

數據庫分庫分表的方式有兩種:一種是垂直拆分,另一種是水平拆分。這兩種方式,在我看來,掌握拆分方式是關鍵,理解拆分原理是內核。所以你在學習時,最好可以結合自身業務來思考。

垂直拆分,顧名思義就是對數據庫豎着拆分,也就是將數據庫的表拆分到多個不同的數據庫中。
垂直拆分的原則一般是按照業務類型來拆分,核心思想是專庫專用,將業務耦合度比較高的表拆分到單獨的庫中。舉個形象的例子就是在整理衣服的時候,將羽絨服、毛衣、T 恤分別放在不同的格子裏。這樣可以解決我在開篇提到的第三個問題:把不同的業務的數據分拆到不同的數據庫節點上,這樣一旦數據庫發生故障時只會影響到某一個模塊的功能,不會影響到整體功能,從而實現了數據層面的故障隔離。

我還是以微博系統爲例來給你說明一下。 在微博系統中有和用戶相關的表,有和內容相關的表,有和關係相關的表,這些表都存儲在主庫中。在拆分後,我們期望用戶相關的表分拆到用戶庫中,內容相關的表分拆到內容庫中,關係相關的表分拆到關係庫中。

對數據庫進行垂直拆分是一種偏常規的方式,這種方式其實你會比較常用,不過拆分之後,雖然可以暫時緩解存儲容量的瓶頸,但並不是萬事大吉,因爲數據庫垂直拆分後依然不能解決某一個業務模塊的數據大量膨脹的問題,一旦你的系統遭遇某一個業務庫的數據量暴增,在這個情況下,你還需要繼續尋找可以彌補的方式。

比如微博關係量早已經過了千億,單一的數據庫或者數據表已經遠遠不能滿足存儲和查詢的需求了,這個時候,你需要將數據拆分到多個數據庫和數據表中,也就是對數據庫和數據表做水平拆分了。

如何對數據庫做水平拆分
和垂直拆分的關注點不同,垂直拆分的關注點在於業務相關性,而水平拆分指的是將單一數據表按照某一種規則拆分到多個數據庫和多個數據表中,關注點在數據的特點。

拆分的規則有下面這兩種:
1. 按照某一個字段的哈希值做拆分,這種拆分規則比較適用於實體表,比如說用戶表,內容表,我們一般按照這些實體表的 ID 字段來拆分。比如說我們想把用戶表拆分成 16 個庫,64 張表,那麼可以先對用戶 ID 做哈希,哈希的目的是將 ID 儘量打散,然後再對 16 取餘,這樣就得到了分庫後的索引值;對 64 取餘,就得到了分表後的索引值。

  2. 另一種比較常用的是按照某一個字段的區間來拆分,比較常用的是時間字段。你知道在內容表裏面有“創建時間”的字段,而我們也是按照時間來查看一個人發佈的內容。我們可能會要看昨天的內容,也可能會看一個月前發佈的內容,這時就可以按照創建時間的區間來分庫分表,比如說可以把一個月的數據放入一張表中,這樣在查詢時就可以根據創建時間先定位數據存儲在哪個表裏面,再按照查詢條件來查詢。

一般來說,列表數據可以使用這種拆分方式,比如一個人一段時間的訂單,一段時間發佈的內容。但是這種方式可能會存在明顯的熱點,這很好理解嘛,你當然會更關注最近我買了什麼,發了什麼,所以查詢的 QPS 也會更多一些,對性能有一定的影響。另外,使用這種拆分規則後,數據表要提前建立好,否則如果時間到了 2020 年元旦,DBA(Database Administrator,數據庫管理員)卻忘記了建表,那麼 2020 年的數據就沒有庫表可寫了,就會發生故障了。

數據庫在分庫分表之後,數據的訪問方式也有了極大的改變,原先只需要根據查詢條件到從庫中查詢數據即可,現在則需要先確認數據在哪一個庫表中,再到那個庫表中查詢數據。這種複雜度也可以通過數據庫中間件來解決,我們在上一節中已經有所講解,這裏就不再贅述了,不過,我想再次強調的是你需要對所使用數據庫中間件的原理有足夠的瞭解和足夠強的運維上的把控能力。

不過,你要知道的是,分庫分表雖然能夠解決數據庫擴展性的問題,但是它也給我們的使用帶來了一些問題。

解決分庫分表引入的問題
分庫分表引入的一個最大的問題就是引入了分庫分表鍵,也叫做分區鍵,也就是我們對數據庫做分庫分表所依據的字段。
從分庫分表規則中你可以看到,無論是哈希拆分還是區間段的拆分,我們首先都需要選取一個數據庫字段,這帶來一個問題是:我們之後所有的查詢都需要帶上這個字段,才能找到數據所在的庫和表,否則就只能向所有的數據庫和數據表發送查詢命令。如果像上面說的要拆分成 16 個庫和 64 張表,那麼一次數據的查詢會變成 16*64=1024 次查詢,查詢的性能肯定是極差的。

當然,方法總比問題多,針對這個問題,我們也會有一些相應的解決思路。比如,在用戶庫中我們使用 ID 作爲分區鍵,這時如果需要按照暱稱來查詢用戶時,你可以按照暱稱作爲分區鍵再做一次拆分,但是這樣會極大的增加存儲成本,如果以後我們還需要按照註冊時間來查詢時要怎麼辦呢,再做一次拆分嗎?

所以最合適的思路是你要建立一個暱稱和 ID 的映射表,在查詢的時候要先通過暱稱查詢到 ID,再通過 ID 查詢完整的數據,這個表也可以是分庫分表的,也需要佔用一定的存儲空間,但是因爲表中只有兩個字段,所以相比重新做一次拆分還是會節省不少的空間的。

分庫分表引入的另外一個問題是一些數據庫的特性在實現時可能變得很困難。比如說多表的 join 在單庫時是可以通過一個 SQL 語句完成的,但是拆分到多個數據庫之後就無法跨庫執行 SQL 了,不過好在我們對於 join 的需求不高,即使有也一般是把兩個表的數據取出後在業務代碼裏面做篩選,複雜是有一些,不過是可以實現的。再比如說在未分庫分表之前查詢數據總數時只需要在 SQL 中執行 count() 即可,現在數據被分散到多個庫表中,我們可能要考慮其他的方案,比方說將計數的數據單獨存儲在一張表中或者記錄在 Redis 裏面。

當然,雖然分庫分表會對我們使用數據庫帶來一些不便,但是相比它所帶來的擴展性和性能方面的提升,我們還是需要做的,因爲,經歷過分庫分表後的系統,才能夠突破單機的容量和請求量的瓶頸,就比如說,我在開篇提到的我們的電商系統,它正是經歷了分庫分表,纔會解決訂單表數據量過大帶來的性能衰減和容量瓶頸。

課程小結
總的來說,在面對數據庫容量瓶頸和寫併發量大的問題時,你可以採用垂直拆分和水平拆分來解決,不過你要注意,這兩種方式雖然能夠解決問題,但是也會引入諸如查詢數據必須帶上分區鍵,列表總數需要單獨冗餘存儲等問題。

而且,你需要了解的是在實現分庫分表過程中,數據從單庫單表遷移多庫多表是一件即繁雜又容易出錯的事情,而且如果我們初期沒有規劃得當,後面要繼續增加數據庫數或者表數時,我們還要經歷這個遷移的過程。所以,從我的經驗出發,對於分庫分表的原則主要有以下幾點:
1. 如果在性能上沒有瓶頸點那麼就儘量不做分庫分表;
2. 如果要做,就儘量一次到位,比如說 16 庫 64 表就基本能夠滿足爲了幾年內你的業務的需求。
3. 很多的 NoSQL 數據庫,例如 Hbase,MongoDB 都提供 auto sharding 的特性,如果你的團隊內部對於這些組件比較熟悉,有較強的運維能力,那麼也可以考慮使用這些 NoSQL 數據庫替代傳統的關係型數據庫。

其實,在我看來,有很多人並沒有真正從根本上搞懂爲什麼要拆分,拆分後會帶來哪些問題,只是一味地學習大廠現有的拆分方法,從而導致問題頻出。所以,你在使用一個方案解決一個問題的時候一定要弄清楚原理,搞清楚這個方案會帶來什麼問題,要如何來解決,要知其然也知其所以然,這樣才能在解決問題的同時避免踩坑。

思考時間
分庫分表實際上是分佈式存儲中一種數據分片的解決方案,那麼你還了解哪些分佈式存儲組件也使用了類似的技術呢?它們實現方式是怎樣的呢?歡迎在留言區與我分享你的經驗。

最後,感謝你的閱讀,如果這篇文章讓你有所收穫,也歡迎你將它分享給更多的朋友。

發佈了198 篇原創文章 · 獲贊 26 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章