手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

大閒人柴毛毛發表於2019-03-01

本文你將學到什麼?

本文是《手把手專案實戰系列》的第二篇文章。上一篇《手把手0基礎教你搭建一套可自動化構建的微服務框架(SpringBoot+Dubbo+Docker+Jenkins)》受到巨大好評,在這裡也深表感謝。應大家要求繼續完成後續章節的撰寫。上一篇的實戰過程介紹的“高喜商城”專案其實是一個真實專案,它是一個標準的線上商城(為了避嫌,“高喜商城”是我隨意起的一個假名字),這個專案的很多技術具有一定的普適性。因此我計劃將它實現的方方面面以專案實戰的形式介紹給大家,讓大家體驗一個真實線上專案的開發、運維、升級過程。

關注訂閱我的文章

相信很多同學對“分庫分表”這一概念一知半解,不用著急,本文的後續章節將會分成知識點掃盲篇實戰動手篇兩部分。知識點掃盲篇將會從零開始,介紹分庫分表的基本知識,然後再帶領大家開始動手實踐。希望能夠給你帶來完美的閱讀體驗。接下來我將用盡量通俗易懂的語言介紹分庫分表的相關知識,不裝逼,做一個低調的程式猿。

預告一下,整個系列會介紹如下內容:

  • 《手把手0基礎專案實戰(一)——教你搭建一套可自動化構建的微服務框架(SpringBoot+Dubbo+Docker+Jenkins)》
  • 《手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰》
  • 《手把手0基礎專案實戰(三)——教你開發一套許可權管理系統》
  • 《手把手0基礎專案實戰(四)——電商訂單系統架構設計與實戰(分散式事務一致性保證)》
  • 《手把手0基礎專案實戰(五)——電商系統的快取策略》
  • 《手把手0基礎專案實戰(六)——基於配置中心實現叢集配置的集中管理和熔斷機制》
  • 《手把手0基礎專案實戰(七)——電商系統的日誌監控方案》
  • 《手把手0基礎專案實戰(八)——基於JMeter的系統效能測試》

知識點掃盲篇

1. 什麼是“分庫分表”?

隨著大資料時代的到來,業務系統的資料量日益增大,資料儲存能力逐漸成為影響系統效能的瓶頸。目前主流的關係型資料庫單表儲存上限為1000萬條記錄,而這一儲存能力顯然已經無法滿足大資料背景下的業務系統儲存要求了。隨著微服務架構、分散式儲存等概念的出現,資料儲存問題也漸漸迎來了轉機。而資料分片是目前解決海量資料持久化儲存與高效查詢的一種重要手段。資料分庫分表的過程在系統設計階段完成,要求系統設計人員根據系統預期的業務量,將未來可能出現瓶頸的資料庫、資料表按照一定規則拆分成多個庫、多張表。這些資料庫和資料表需要部署在不同的伺服器上,從而將資料讀寫壓力分攤至叢集中的各個節點,提升資料庫整體處理能力,避免出現讀寫瓶頸的現象。

目前資料分片的方式一共有兩種:離散分片和連續分片。

離散分片是按照資料的某一欄位雜湊取模後進行分片儲存。只要雜湊演算法選擇得當,資料就會均勻地分佈在不同的分片中,從而將讀寫壓力平均分配給所有分片,整體上提升資料的讀寫能力。然而,離散儲存要求資料之間有較強的獨立性,但實際業務系統並非如此,不同分片之間的資料往往存在一定的關聯性,因此在某些場景下需要跨分片連線查詢。由於目前所有的關係型資料庫出於安全性考慮,均不支援跨庫連線。因此,跨庫操作需要由資料分庫分表中介軟體來完成,這極大影響資料的查詢效率。此外,當資料儲存能力出現瓶頸需要擴容時,離散分片規則需要將所有資料重新進行雜湊取模運算,這無疑成為限制系統可擴充套件性的一個重要因素。雖然,一致性雜湊能在一定程度上減少系統擴容時的資料遷移,但資料遷移問題仍然不可避免。對於一個已經上線執行的系統而言,系統停止對外服務進行資料遷移的代價太大。

第二種資料分片的方式即為連續分片,它能解決系統擴容時產生的資料遷移問題。這種方式要求資料按照時間或連續自增主鍵連續儲存。從而一段時間內的資料或相鄰主鍵的資料會被儲存在同一個分片中。當需要增加分片時,不會影響現有的分片。因此,連續分片能解決擴容所帶來的資料遷移問題。但是,資料的儲存時間和讀寫頻率往往呈正比,也就是大量的讀寫往往都集中在最新儲存的那一部分資料,這就會導致熱點問題,並不能起到分攤讀寫壓力的初衷。

2. 資料庫擴充套件的幾種方式

資料庫擴充套件一共有四種分配方式,分別是:垂直分庫、垂直分表、水平分表、水平資料分片。每一種策略都有各自的適用場景。

  1. 垂直分庫

    垂直分庫即是將一個完整的資料庫根據業務功能拆分成多個獨立的資料庫,這些資料庫可以執行在不同的伺服器上,從而提升資料庫整體的資料讀寫效能。這種方式在微服務架構中非常常用。微服務架構的核心思想是將一個完整的應用按照業務功能拆分成多個可獨立執行的子系統,這些子系統稱為“微服務”,各個服務之間通過RPC介面通訊,這樣的結構使得系統耦合度更低、更易於擴充套件。垂直分庫的理念與微服務的理念不謀而合,可以將原本完整的資料按照微服務拆分系統的方式,拆分成多個獨立的資料庫,使得每個微服務系統都有各自獨立的資料庫,從而可以避免單個資料庫節點壓力過大,影響系統的整體效能,如下圖所示。

    手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

  2. 垂直分表

    垂直分表如果一張表的欄位非常多,那麼很有可能會引起資料的跨頁儲存,這會造成資料庫額外的效能開銷,而垂直分表可以解決這個問題。垂直分表就是將一張表中不常用的欄位拆分到另一張表中,從而保證第一章表中的欄位較少,避免出現資料庫跨頁儲存的問題,從而提升查詢效率。而另一張表中的資料通過外來鍵與第一張表進行關聯,如下圖所示。

    手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

  3. 水平分表

    如果一張表中的記錄數過多(超過1000萬條記錄),那麼會對資料庫的讀寫效能產生較大的影響,雖然此時仍然能夠正確地讀寫,但讀寫的速度已經到了業務無法忍受的地步,此時就需要使用水平分表來解決這個問題。水平分表是將一張含有很多記錄數的表水平切分,拆分成幾張結構相同的表。舉個例子,假設一張訂單表目前儲存了2000萬條訂單的資料,導致資料讀寫效率極低。此時可以採用水平分表的方式,將訂單表拆分成100張結構相同的訂單表,分別叫做order_1、order_2……、order_100。然後可以根據訂單所屬使用者的id進行雜湊取模後均勻地儲存在這100張表中,從而每張表中只儲存了20萬條訂單記錄,極大提升了訂單的讀寫效率,如下圖所示。 當然,如果拆分出來的表都儲存在同一個資料庫節點上,那麼當請求量過大的時候,畢竟單臺伺服器的處理能力是有限的,資料庫仍然會成為系統的瓶頸,所以為了解決這個問題,就出現了水平資料分片的解決方案。

    手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

  4. 水平分庫分表

    水平資料分片與資料分片區別在於:水平資料分片首先將資料表進行水平拆分,然後按照某一分片規則儲存在多臺資料庫伺服器上。從而將單庫的壓力分攤到了多庫上,從而避免因為資料庫硬體資源有限導致的資料庫效能瓶頸,如下圖所示。

    手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

3. 分庫分表的幾種方式

目前常用的資料分片策略有兩種,分別是連續分片和離散分片。

  1. 離散分片

    離散分片是指將資料打散之後均勻地儲存在邏輯表的各個分片中,從而使的對同一張邏輯表的資料讀取操作均勻地落在不同庫的不同表上,從而提高讀寫速度。離散分片一般以雜湊取模的方式實現。比如:一張邏輯表有4個分片,那麼在讀寫資料的時候,中介軟體首先會取得分片欄位的雜湊值,然後再模以4,從而計算出該條記錄所在的分片。在這種方法中,只要雜湊演算法選的好,那麼資料分片將會比較均勻,從而資料讀寫就會比較均勻地落在各個分片上,從而就有較高的讀寫效率。但是,這種方式也存在一個最大的缺陷——資料庫擴容成本較高。採用這種方式,如果需要再增加分片,原先的分片演算法將失效,並且所有記錄都需要重新計算所在分片的位置。對於一個已經上線的系統來說,行級別的資料遷移成本相當高,而且由於資料遷移期間系統仍在執行,仍有新資料產生,從而無法保證遷移過程資料的一致性。如果為了避免這個問題而停機遷移,那必然會對業務造成巨大影響。當然,如果為了避免資料遷移,在一開始的時候就分片較多的分片,那需要承擔較高的費用,這對於中小公司來說是無法承受的。

  2. 連續分片

    連續分片指的是按照某一種分片規則,將某一個區間內的資料儲存在同一個分片上。比如按照時間分片,每個月生成一張物理表。那麼在讀寫資料時,直接根據當前時間就可以找到資料所在的分片。再比如可以按照記錄ID分片,這種分片方式要求ID需要連續遞增。由於Mysql資料庫單表支援最大的記錄數約為1000萬,因此我們可以根據記錄的ID,使得每個分片儲存1000萬條記錄,當目前的記錄數即將到達儲存上限時,我們只需增加分片即可,原有的資料無需遷移。連續分片的一個最大好處就是方便擴容,因為它不需要任何的資料遷移。但是,連續分片有個最大的缺點就是熱點問題。連續分片使得新插入的資料集中在同一個分片上,而往往新插入的資料讀寫頻率較高,因此,讀寫操作都會集中在最新的分片上,從而無法體現資料分片的優勢。

4. 引入分庫分表中介軟體後面臨的問題

  1. 跨庫操作

    在關係型資料庫中,多張表之間往往存在關聯,我們在開發過程中需要使用JOIN操作進行多表連線。但是當我們使用了分庫分表模式後,由於資料庫廠商處於安全考慮,不允許跨庫JOIN操作,從而如果需要連線的兩張表被分到不同的庫中後,就無法使用SQL提供的JOIN關鍵字來實現表連線,我們可能需要在業務系統層面,通過多次SQL查詢,完成資料的組裝和拼接。這一方面會增加業務系統的複雜度,另一方面會增加業務系統的負載。 因此,當我們使用分庫分表模式時,需要根據具體的業務場景,合理地設定分片策略、設定分片欄位,這將會在本文的後續章節中介紹。

  2. 分散式事務

    我們知道,資料庫提供了事務的功能,以保證資料一致性。然而,這種事務只是針對單資料庫而言的,資料庫廠商並未提供跨庫事務。因此,當我們使用了分庫分表之後,就需要我們在業務系統層面實現分散式事務。關於分散式事務的詳細內容,可以參考筆者的另一篇文章《常用的分散式事務解決方案》

5. 現有分庫分表中介軟體的橫向對比

  • Cobar實現資料庫的透明分庫,讓開發人員能夠在無感知的情況下操縱資料庫叢集,從而簡化資料庫的程式設計模型。然而Cobar僅實現了分庫功能,並未實現分表功能。分庫可以解決單庫IO、CPU、記憶體的瓶頸,但無法解決單表資料量過大的問題。此外,Cobar是一個獨立執行的系統,它處在應用系統與資料庫系統之間,因此增加了額外的部署複雜度,增加了運維成本。

  • 為了解決上述問題,Cobar還推出了一個Cobar-Client專案,它只是一個安裝在應用程式的Jar包,並不是一個獨立執行的系統,一定程度上降低了系統的複雜度。但和Cobar一樣,仍然只支援分庫,並不支援分表,也不支援讀寫分離。

  • MyCat是基於Cobar二次開發的資料庫中介軟體,和Cobar相比,它增加了讀寫分離的功能,並修復了Cobar的一些bug。但是,MyCat和Cobar一樣,都是一套需要獨立部署的系統,因此會增加部署的複雜度,提高了後期系統運維的成本。


實戰篇

1. 為何要進行分庫分表?

高喜商城已經上線了一段時間,使用者量超預期增長,業務層採用基於Dubbo的微服務架構,並結合了Docker+Jenkins實現了自動化部署,具備靈活的擴充套件能力,能夠輕鬆支撐目前的業務量。然而,資料庫層面卻出現了瓶頸。由於1.0版本採用單庫單表設計,雖然使用Mysql讀寫分離實現了一主多備架構,一定程度上分攤了資料庫的讀寫壓力。但按照目前的業務發展速度,很多業務表將會面臨單表過長的問題。目前Mysql資料庫在保證讀寫效能的前提下,單表最大支援1000W條資料。當單表超過1000W條資料後,雖然仍然可以儲存資料,但讀寫效能大幅下降。因此,為了滿足極速增長的業務需求,需要使用資料庫中介軟體實現資料分庫分表儲存。分庫能將讀寫壓力分攤至不同節點,從而緩解讀寫壓力;而分表能夠避免單表過長的問題。此外,大多數分庫分表中介軟體都會提供讀寫分離的功能,從而進一步緩解資料庫的讀寫壓力,提升讀寫效能。

綜上所述,對資料庫進行分庫分表迫在眉睫!

2. 高喜商城1.0資料庫架構介紹

高喜商城1.0的架構如下圖所示:

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

該架構的業務層採用微服務架構,所有將整個應用分成四個業務系統:使用者系統、產品系統、訂單系統和資料分析系統。關於微服務架構這裡不做過多介紹,詳細內容請閱讀《手把手0基礎專案實戰(一)——教你搭建一套可自動化構建的微服務框架(SpringBoot+Dubbo+Docker+Jenkins)》,這裡主要介紹資料庫架構。

在高喜商城1.0版本中,雖然業務層採用微服務架構,業務層被拆分成多個相互獨立的子系統,但在資料庫層,整個系統的所有表均在同一個資料庫中儲存。此外,採用資料庫的主從複製實現了讀寫分離,資料庫有一個主庫和兩個從庫組成了一個資料庫叢集。它是一個對等叢集,每個庫中儲存的資料是一致的。

在加入了讀寫分離後,一方面提升了資料庫的讀寫效能;另一方面,實現了資料庫的高可用。當某一個節點發生故障時,仍然有其他兩個節點提供服務。

這種架構存在如下幾個缺點:

  1. 沒有垂直分庫:所有業務系統的表均儲存在同一個庫中,相互之間沒有任何隔離,從而導致一個業務系統可以直接讀寫其他業務系統的資料,這違背了微服務的理念。
  2. 存在單表過長的問題:系統經過一段時間運營後,有些表的資料量較大,單表資料量可能會超過1000W。這將會極大影響該表的讀寫效能。

針對上述問題,對資料庫進行分庫分表迫在眉睫。

3. 高喜商城2.0資料庫架構的演進

高喜商城2.0資料庫架構如下圖所示:

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

在2.0架構中,首先對資料庫進行了垂直拆分,每個子系統均擁有自己獨立的資料庫,不同系統的資料庫相互隔離,無法互相訪問。這樣保證了各個業務系統的純粹性,不同業務系統之間如果需要資料互動,那麼就通過業務系統提供了RPC介面訪問,而非通過資料庫訪問,從而符合微服務的設計理念。

上圖對使用者系統的資料庫架構做了詳細介紹,其他系統的資料庫架構和使用者系統類似,都採用了分庫分表+讀寫分離的架構。

在使用者系統中,資料庫一共被分成N個主庫和N個從庫,每個庫中的表又被拆分成多張。以上圖為例,使用者系統的資料庫一共被分成兩個物理庫,分別是db_0和db_1。此外,為了實現讀寫分離,每個物理庫均擁有一個從庫,主從資料庫的資料保持一致。從而,使用者系統的物理庫一共被分成四個,分別是:db_0_master、db_1_master、db_0_slave、db_1_slave。

每個庫中的表table被水平拆分成兩張,分別是table_0、table_1。從而,原本一張table表被水平拆分成了四張,分別是:db_0_master_table_0、db_0_master_table_1、db_1_master_table_0、db_1_master_table_1。與此同時,從庫中也有四張這樣的table表,並且和主庫的資料保持一致,因此,經過水平拆分後,一共有8張table表。

上述table表只是舉一個例子,實際每個系統均包含有多張表,每張表的拆分規則和拆分數量要根據該表具體的業務量來決定。具體的拆分過程將在下面介紹。

4. 高喜商城1.0資料庫表結構設計

下面將會詳細介紹高喜商城資料表的設計。這些設計在線上商城系統中是通用的,具備一定的借鑑意義,因此下面將會詳細介紹。

4.1 使用者系統資料表

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

使用者系統的資料表一共由如上六張表構成,下面對這六張表的作用以及相互之間的關係作簡單介紹。

  • sys_user:使用者表。
    • 儲存使用者的基本資訊。
  • sys_role:角色表。
    • 儲存本系統中所有的角色,如:超級管理員、普通使用者、企業使用者等等。
    • 使用者和角色之間是多對一的聚合關係,即一個使用者只能擁有一種角色,而一種角色卻可以屬於多個使用者。由於角色可以脫離使用者單獨存在,因此他們之間是一種弱依賴關係——聚合關係。
  • sys_permission:許可權表。
    • 儲存本系統的許可權資訊,如:建立角色、刪除角色、建立選單、刪除選單、修改使用者資訊等等。
    • 角色和許可權是多對多的聚合關係,即一種角色可以擁有多種許可權,而一種許可權也可以屬於多種角色。並且由於許可權可以脫離角色單獨存在,因此他們之間是弱依賴關係——聚合關係。
    • 更多關於本系統許可權管理功能的設計,請關注後面即將推出的《手把手0基礎教你實現一套許可權管理系統》。
  • sys_menu:選單表。
    • 儲存本系統的選單資訊。
    • 由於需要實現角色看到不同的選單,因此需要建立這張選單表,儲存本系統所有的選單資訊。
    • 角色和選單是多對多的聚合關係,即一種角色可以擁有多個選單,而一個選單也可以屬於多種角色。並且由於選單可以脫離於角色單獨存在,因此他們之間是弱依賴關係——聚合關係。
  • location:使用者地址資訊表。
    • 儲存使用者的地址資訊。
    • 使用者下單之後需要填寫收貨地址,因此需要這張表儲存使用者的地址資訊。
    • 使用者和地址之間是一對多的組合關係,即一個使用者可以擁有多個收貨地址,並且一個收貨地址只能屬於一個使用者。此外,由於收穫地址不能脫離於使用者單獨存在,因此他們之間是強依賴關係——組合關係。
  • receipt:發票表。
    • 使用者在下單時可以填寫發票資訊,因此需要這張表來儲存這些發票資訊。
    • 使用者和發票之間是一對多的組合關係,即一個使用者可以擁有多個發票資訊,而一個具體的發票資訊只能屬於一個使用者。此外,由於發票不能脫離於使用者單獨存在,因此他們之間是強依賴關係——組合關係。

到此為止,使用者系統的每一張表及表於表之間的關係都已詳細介紹完畢。通過這些表以及表之間的關係我們就能看出使用者系統的業務需求。

  • 每一個使用者都有且僅有一種確定的角色,該角色對應了若干個選單和若干種許可權。當使用者登入系統的時候,使用者系統就可以根據資料庫中儲存的這些使用者資訊,知道該使用者能夠看到哪些選單,然後將這些選單顯示在使用者的介面上。此外,當使用者操作這個系統時,前端就會呼叫相應的後臺介面,每次呼叫任何介面時,使用者系統都會根據使用者的許可權資訊檢測該使用者是否具有操作該介面的許可權,如果沒有許可權則拒絕執行,從而保證系統的安全性。
  • 一個使用者在下單的時候可以要求開具發票,那麼這些發票資訊將會被儲存在receipt表中,當使用者再次下單的時候,我們就會查詢receipt表,將該使用者所有的發票資訊展示給他,供使用者選擇。
  • 一個使用者在下單的時候需要填寫收貨地址,那麼這些收穫地址就會被儲存在location表中,當使用者再次下單時,無需再次輸入收穫地址,我們的系統會查詢location表,讓使用者直接選擇。

4.2 產品系統資料表

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

產品系統的資料表一共由如上四張表構成,下面對這四張表的作用以及相互之間的關係作簡單介紹。

  • product:產品表。
    • 儲存本系統所有的產品資訊。
  • prod_image:產品圖片表。
    • 儲存本系統所有的產品圖片URL。
    • 產品和圖片之間是一對多組合關係,即一個產品能夠擁有多張圖片,而一張圖片只能屬於某一個產品,並且圖片不能脫離於產品單獨存在,因此他們之間是強依賴關係——組合關係。
  • brand:品牌表。
    • 儲存本系統中所有的品牌資訊。
    • 產品和品牌是多對一的聚合關係,即一個產品只屬於一種品牌,而一種品牌可以包含多個產品。並且品牌可以獨立於產品單獨存在,因此他們之間是弱依賴關係——聚合關係。
  • category:類別表。
    • 每件產品都必須屬於一個類別,因此通過類別表來儲存所有的類別資訊。
    • 產品和類別之間是多對一的聚合關係,即一件產品只能屬於一種類別,而一種類別卻可以包含多件產品。並且類別可以獨立於產品存在,因此他們之弱依賴關係——聚合關係。

4.3 訂單系統資料表

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

訂單系統的資料表一共由如上三張表構成,下面對這三張表的作用以及相互之間的關係作簡單介紹。

  • orders:訂單表。
    • 儲存本系統所有使用者的訂單資訊。
  • orders_product:訂單中的產品表。
    • 每條訂單中一般都包含多件產品,這種對映關係就儲存在這張表中。
    • 這張表是訂單和產品之間的關聯表。
    • 訂單和產品之間是多對多的聚合關係,即一條訂單中可以包含多件產品,並且一件產品也可以屬於多條訂單。此外,由於產品可以獨立於訂單而存在,因此他們之間是弱依賴關係——聚合關係。
    • 訂單和訂單產品表是一對多組合關係。因為,一條訂單中往往包含多個產品,而一條訂單產品對映只能屬於某一條具體的訂單。並且訂單產品不能獨立於訂單而存在,因此他們之間是強依賴關係——組合關係。
  • order_state_time:訂單中各種狀態發生時間表。
    • 一條訂單有多種狀態,如:已下單、未支付、已支付、發貨中、已收穫等等。為了能夠詳細記錄訂單每個狀態的發生時間,因此需要這張order_state_time表。
    • 訂單和訂單狀態之間是一對多的組合關係,即一條訂單可以包含多種訂單狀態時間,而一種訂單狀態時間只能屬於某一條具體的訂單。並且訂單狀態時間不能獨立於訂單而存在,因此他們之間是強依賴關係——組合關係。

到此為止,一個線上商城中最核心的三大系統的資料表關係已經梳理清楚了,下面將會根據具體的業務指標,對這些資料庫和資料表進行合理的分庫分表。

5. 高喜商城2.0分庫分表方案

在對高喜商城開始分庫分表之前,我們先要搞清楚,究竟為何要分庫?為何要分表?為何要讀寫分離?

  • 分庫的目的:將對同一個庫的讀寫壓力分攤到多個庫上,不同庫分佈在不同的伺服器上,從而緩解每個庫上的讀寫壓力,避免因伺服器硬體資源(如IO、記憶體、CPU)導致的瓶頸。
  • 分表的目的:將原本一張表中的資料水平拆分至多張表中,從而避免單表過長,提升讀寫效能。
  • 讀寫分離的目的:將一個物理庫複製多份,主庫負責寫操作,從庫負責讀操作。從而避免少量的寫操作的表鎖或行鎖阻塞了大量的讀操作,通過降低資料的一致性來提升讀操作的效能。

5.1 使用者系統的分庫分表方案

系統的分庫分表策略一定是基於具體的業務指標和實際的業務需求,在正式進行分庫分表策略的設計之前,一定要做好這兩部分資料的採集。現在高喜商城的業務指標和業務需求如下面兩張表格所示:

高喜商城未來五年的業務指標:

表名 未來五年資料量 關鍵欄位
sys_user 1000W uid, username, password, email, phone, role_id
location 5000W uid, location
receipt 5000W uid, 發票相關欄位
sys_role 100 role_id, role_name
sys_permission 1000 pms_id, permission
sys_role_permission 100*1000 role_id, pms_id
sys_menu 200 menu_id, menu
sys_role_menu 200*1000 role_id, menu_id

高喜商城的業務需求:

表名 業務需求 涉及欄位
sys_user 1.使用者登入(使用者名稱登入) username, password
2.使用者登入(郵箱登入) email, password
3.使用者登入(簡訊驗證碼登入) phone
4.根據uid查詢使用者資訊 uid
5.管理員按照某些條件分頁查詢使用者 任何欄位都有可能使用
表名 業務需求 涉及欄位
location 根據uid查詢收貨地址 uid
表名 業務需求 涉及欄位
receipt 根據uid查詢發票資訊 uid

高喜商城未來五年預計將會擁有1000萬使用者,從而使用者表將會有1000萬條資料。由於目前Mysql單表支援最大長度為1000萬,因此為了保險起見,我們需要將使用者表水平拆分成兩張。此外,為了防止使用者表讀寫壓力過大,我們乾脆將這兩張使用者表放入兩個物理庫中。並且為了保證使用者表的高可用,我們對這兩個資料庫採用主從複製技術,一主一叢,其結構如下圖所示:

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

從高喜商城未來五年的業務量表中可知,系統的角色、許可權、選單數量較少,沒有必要分庫分表。在使用者查詢的過程中需要連線使用者表、角色表、許可權表和選單表,如果將這些無需拆分的表儲存在某一個資料庫中,那麼使用者表將無法和他們進行跨庫連線,從而需要在完成使用者資訊查詢後,在業務層再次根據uid分別查詢角色資訊、許可權資訊、選單資訊,這無意增加了業務層的實現複雜度。為了解決這個問題,我們可以對角色表、許可權表和選單表進行冗餘,即將這些表冗餘地儲存在sys_user的所有物理庫中,從而任何一個物理庫的使用者查詢操作都可以直接通過表連線的方式完成角色資訊、許可權資訊和選單資訊的查詢。其結構如下圖所示:

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

此外,使用者和收穫地址、使用者和發票資訊之間都是一對多的組合關係,如果每個使用者平均擁有5個收貨地址和5種發票資訊,那麼對於1000萬使用者而言,一共會建立5000萬條收穫資訊和5000萬條發票資訊。因此,收穫地址和發票資訊各需6張表來儲存。並且,由於這兩種資訊都是通過uid來查詢,並且查詢條件只有uid這一項,因此uid毫無爭議地成為分片欄位,並且這6張表只能分佈在6個物理庫中。此外,為了實現資料庫的高可用性,需要對這6個庫提供主從複製功能。最終,收貨地址表和發票資訊表的結構如下圖所示:

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

高喜商城使用者系統的資料庫分庫分表方案就介紹到這,下面介紹產品系統的分庫分表方案。

5.2 訂單系統的分庫分表方案

和使用者系統的分庫分表方案設計過程一樣,在方案設計之前,首先要確定系統的業務指標和業務需求。

高喜商城未來五年的業務指標:

表名 未來五年資料量 關鍵欄位
orders 2000W order_id, buyer_id, seller_id
orders_product 5*2000W order_id, prodcut_id
order_state_time 10*2000W order_id, state, time
  • 根據運營同學的估算,高喜商城未來五年的訂單量最多將會達到2000W條,並且平均每條訂單中包含5件商品,因此orders_product表中的資料量將會達到10000W;並且每條訂單都有10種狀態,因此order_state_time表的資料量將會達到20000W。
  • 基於上述資料,orders需要水平拆分成4張物理表,orders_product需要水平拆分成20張物理表,order_state_time需要水平拆分成40張物理表。
  • 那麼這寫物理表究竟該分配給多少個物理庫中?這需要由業務需求來決定。

高喜商城的業務需求:

表名 業務需求 涉及欄位
orders 1.根據buyer_id分頁查詢某一使用者的訂單 buyer_id
2.根據order_id查詢訂單 order_id
3.根據seller_id分頁查詢某一商家的訂單 seller_id
表名 業務需求 涉及欄位
orders_product 根據order_id查詢產品列表 order_id
表名 業務需求 涉及欄位
order_state_time 1.根據order_id和state篩選某一狀態下的訂單 order_id, state
2.修改指定訂單的狀態 order_id, state

訂單系統的核心業務需求如上述三張表所示。orders表和orders_product表、order_state_time表之間都是一對多的組合關係,在查詢過程中需要進行表連線操作。因此,我們必須要指定合理的分庫分表方案,能夠使得同一訂單的產品資訊、訂單狀態資訊都落在同一個物理庫中,從而能夠直接使用SQL語句進行連線操作。如果分庫分表方案不合理,那麼同一訂單的產品資訊和訂單狀態資訊會散落在不同的物理庫中,由於Mysql並不支援跨庫連線,因此這三張表的連線需要拆分成三次資料庫查詢,並在業務層完成資料的連線,這無意增加了業務層的複雜度。下面詳細介紹訂單系統的分庫分表方案。

通過分析上述三張業務需求表可知,訂單系統核心操作所涉及到的欄位無非就是三個:order_id、buyer_id、seller_id。當查詢指定訂單的時候需要使用order_id作為查詢條件,當查詢某一買家所有訂單的時候需要使用buyer_id作為查詢條件,當查詢某一賣家所有訂單的時候需要使用seller_id作為查詢條件。由此可見,分片欄位需要從這三個欄位中選擇。那麼究竟應該如何選擇呢?我們分別來看如下三種方案:

  1. 將order_id作為分片欄位 如果將order_id作為分片欄位,那麼根據order_id查詢指定訂單的時候可以直接定位到指定的物理表,然而在根據buyer_id和seller_id查詢訂單的時候,由於無法定位到具體的表,因此就需要全庫表查詢,這顯然是低效的。

  2. 將buyer_id作為分片欄位 此時,查詢指定買家的訂單資訊可以直接定位到指定的物理表,但是當需要根據order_id查詢具體訂單資訊、查詢賣家訂單資訊時就顯得提襟見肘了。

  3. 將seller_id作為分片欄位 此時,查詢指定賣家的所有訂單資訊可以定位到指定的物理表,但查詢買家訂單、根據訂單編號查詢訂單時就需要全庫表查詢了。

綜上所述,如果只將這三個欄位中的某一個作為分片欄位,顯然無法滿足所有的業務場景,必定會存在全庫表查詢,這就會導致查詢效率低下。那麼,有沒有什麼方案能夠避免全庫表查詢呢?當然有!

首先,我們來解決跨庫連線的問題。

解決跨庫連線問題的根本方法就是避免跨庫連線,讓需要連線的表儲存在同一個物理庫中。在訂單系統中,orders表要分別和orders_product表、order_state_time表產生連線,並且都是以order_id作為連線欄位。但是,如果我們以order_id作為這三張表的分片欄位,那麼當根據buyer_id、seller_id查詢時,都需要全庫表操作。所以,我們需要分別以buyer_id和seller_id作為分片欄位。聽上去很神奇,具體怎麼實施呢?

在訂單關係中,買家(buyer_id)和賣家(seller_id)是多對多的聚合關係,對於多對多關係,我們可以使用表冗餘來實現不同緯度的查詢。

此時,我們需要將訂單表(orders)一分為二,分別是買家訂單表(orders_buyer)賣家訂單表(orders_seller),這兩張表的資料是完全一致的。在買家訂單表中,以buyer_id作為分片欄位;在賣家訂單表中,以seller_id作為分片欄位。那麼當需要查詢指定買家的訂單時,根據買家id(buyer_id)就可以確定該買家訂單資料所在的物理表;當需要查詢指定賣家的訂單時,根據賣家id(seller_id)就可以確定該賣家訂單資料所在的物理表。

此時已經避免了上述兩個業務場景的全庫表查詢,那麼還有兩種業務場景的全庫表查詢問題如何解決呢?

  1. 根據order_id查詢訂單
  2. orders表要分別和orders_product表、order_state_time表的連線操作
  • 對於第一個問題,買家(buyer_id)和訂單(order_id)之間是一對多組合關係。對於一對多組合關係,我們可以建立“多”——>“一”的對映。在這裡,我們需要建立order_id——>buyer_id的對映關係。那麼當需要根據order_id查詢訂單的時候,首先需要查詢這個對映關係,找到order_id對應的buyer_id,由於buyer_id是分片欄位,因此可以直接計算出資料所在的物理表,從而完成根據order_id查詢訂單的需求。
  • 對於第二個問題,我們可以使用欄位冗餘的方法來解決。在建立買家訂單表和賣家訂單表的同時,再分別建立如下四個表:
    • 買家訂單產品表(orders_product_buyer)
      • (order_id, product_id, buyer_id)
      • 加入buyer_id欄位,並以buyer_id作為分片欄位
    • 賣家訂單產品表(orders_product_seller)
      • (order_id, product_id, seller_id)
      • 加入seller_id欄位,並以seller_id作為分片欄位
    • 買家訂單狀態表(order_state_time_buyer)
      • (order_id, state, time, buyer_id)
      • 加入buyer_id欄位,並以buyer_id作為分片欄位
    • 賣家訂單狀態表(order_state_time_seller)
      • (order_id, state, time, seller_id)
      • 加入seller_id欄位,並以seller_id作為分片欄位

此時,訂單系統的資料庫架構如下圖所示:

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

採用了上述方案後,所有的全庫表查詢問題都得到了解決,但不要止步於此,還可以進一步優化。

上述方案中,我們使用了一張對映表來維護order_id和buyer_id之間的對映關係,當需要根據order_id查詢指定訂單的時候,先要查詢對映表,找到該訂單對應的buyer_id,然後再根據buyer_id計算分片,找到相應的物理表。

這個過程經歷了兩次地址查詢,還需要額外的策略儲存對映表。那麼,有沒有什麼方法能夠解決這兩個問題呢?當然是有的,此時就要介紹我的黑科技了。

x%N的結果其實是由x二進位制的末尾logN位決定的

舉個例子,13534443 % 8,其實是由13534443的二進位制表示法的最後log8位決定的。

  • 13534443的二進位制是:110011101000010011101011
  • log8=3

因此,13534443 % 8的結果由011決定。也就是說,只要末尾三位都是011的數字,對8取模的結果都是一樣的。

基於這個結論,我們只要保證buyer_id和order_id的最後logN位一致,就無需再使用額外的對映表來儲存這兩者的對映關係。order_id和buyer_id的生成方式如下:

  1. 在建立訂單時,首先獲取買家的uid
  2. 獲取uid二進位制表示法的最後logN位,用lastN表示
  3. 將UUID+lastN作為order_id

此時同一個買家的buyer_id%N的結果和order_id%N的結果一致。在根據order_id查詢訂單的時候直接通過order_id%N計算出訂單所在的物理庫即可。

5.3 產品系統的分庫分表方案

高喜商城未來五年的業務指標:

表名 未來五年資料量 關鍵欄位
product 100W product_id
prod_image 10*100W product_id
brand 1000
category 100

總體而言,產品系統的資料量相對較小。運營同學規劃,未來五年,高喜商城的產品數量最多為100W,由於每件產品最多允許擁有10張圖片,因此prod_image表的數量預計為1000W,因此需要對prod_image表進行拆分;而產品的品牌、產品的類別數量較小,不需要考慮分庫分表。

產品系統的資料庫分庫分表方案如下圖所示:

手把手0基礎專案實戰(二)——微服務架構下的資料庫分庫分表實戰

由於prod_image表的資料量將會達到1000W,因此為了避免單表資料超過1000W,將該表根據prod_id拆分成兩張物理表。

此外,在產品系統中,product、brand、category資料量均不會超過1000W,因此無需分庫分表。

關注訂閱我的文章

相關文章