數據庫的水平拆分


數據庫的水平拆分

比如說訂單,你第一要考慮業務場景,查詢訂單是哪些用戶:其一是前端的用戶;其二是后端的用戶商家和客服。
第二,它的存儲量,訂單的數據量是非常大的。但對商品和庫存來說,它是有一定的范圍的,不會無限的大,因為一個網站或者一個商店,你賣的SKU數量是
有限的。一個大超市可能是幾萬個SKU,一個小門店可能是幾百個,它不會無限擴展的。

數據增量也是如此,一個大超市賣的SKU也就是幾萬個,電商平台可能是百萬級千萬級,但是它也不是無限增長的,這更多取決於商家的體量,所以它的數據
量即使有增長也是非常緩慢的。這和訂單不一樣,訂單是幾何式的增長。

再看讀和寫,訂單、庫存的讀和寫頻率都很高。但是對於商品、價格來講,讀肯定是很高的,因為不停地在瀏覽,但是寫是很少的,改價格的機率很低,不
停地改商品信息的機率也是很低的。

另外是事務的一致性。對於訂單和庫存一定是要保持一致性,商品信息寫的話比較少,不太涉及到事務,除非是批量修改,相對來說事務性一致性稍微弱一
些。

還有緩存,庫存可以有緩存,但是緩存的時間是很短的,庫存的緩存時效不可能是以天、以小時為級別的,幾分鍾級別已經是不錯了。很多時候前端顯示還
是有庫存,后面可能已經沒有了,所以庫存有時效性的要求。但為了減輕數據庫壓力,在前端展示會有庫存的緩存,比如有時候大家會遇到,在瀏覽的時候
發現它是有庫存的,但是下單就沒有了,那就是因為前端是緩存的,但是下單的是實時的庫存,已經沒有了。但對商品和價格信息來說,緩存時效就可以長
一些,可以通過緩存技術減輕數據庫的壓力。

熱點數據也是一樣的。數據庫的水平拆分怎么拆,從哪些維度去拆,比如說訂單,可以有幾個維度,你可以根據訂單號去拆,根據產用戶、商家去拆。對響
應速度來說,用戶要求響應速度是最高的;而對商家來說,用戶下完訂單之后,稍微延遲一會兒他也能接受。

如果按照用戶去拆,熱點數據的概率就很低,很難出現一個用戶一下子出現幾千個幾萬個訂單;但是如果按對商家來拆,有一些大的商家,一個雙十一可能
幾個小時就有上千萬的單,這個量就非常大。

而對商品信息來說,如果說你的量沒有像天貓、淘寶級別的話,並且主要是靠緩存來讀,一般的電商網站,是不需要拆分的。

在做訂單水平拆庫的時候,不可能網站停下來去做這個項目,所以我們說是飛機開的時候換發動機,在汽車跑的時候換輪胎。

一個是Service化做了技術架構上的拆分,一個是做了數據庫的水平拆分。這是剛剛提到的准備工作,Service化和水平拆庫的同時,我們的很多中間件技術
也發展起來了,因為你的量上來了、架構調整了,配套設施也要上來,不是說簡單的教室一拆分就完了,學校沒有保安,要上體育課沒有操場是不行的,因
此沒有相應的中間件沒有是不行的。

數據庫拆分有兩種:

1)垂直分庫 
數據庫里的表太多,拿出部分到新的庫里,一般是根據業務划分表,關系密切的表放同一數據庫,應用修改數據庫連接即可,比較簡單。 
2)水平分庫 
某張表太大,單個數據庫存儲不下或訪問性能有壓力,把一張表拆成多張,每張表存放部分記錄,保存在不同的數據庫里,水平分庫需要對系統做大的改造。

1)Scale up,升級Oracle數據庫所在的物理機,提升內存/存儲/IO性能,但這種升級費用昂貴,並且只能滿足短期需要。 
2)Scale out,把訂單庫拆分為多個庫,分散到多台機器進行存儲和訪問,這種做法支持水平擴展,可以滿足長遠需要。

訂單庫主要包括訂單主表/訂單明細表(記錄商品明細)/訂單擴展表,水平分庫即把這3張表的記錄分到多個數據庫中,訂單水平分庫效果如下圖所示:

原來一個Oracle庫被多個MySQL庫取代,支持1主多備和讀寫分離,主備之間通過MySQL自帶的數據同步機制(SLA<1秒),所有應用通過訂單服務訪問訂單數據。

分庫維度

水平分庫首先要考慮根據哪個字段作為分庫維度,選擇標准是盡量避免應用代碼和SQL性能受影響,這就要求當前SQL在分庫后,訪問盡量落在單個庫里,否則單庫訪問變成多庫掃描,讀寫性能和應用邏輯都會受較大影響,。

對於訂單拆分,大家首先想到的是按照用戶Id拆分,結論沒錯,但最好還是數據說話,不能拍腦袋。好的做法是首先收集所有SQL,挑選where語句最常出現的過濾字段,比如用戶Id/訂單Id/商家Id,每個字段在SQL中有三種情況:

  • 單Id過濾,如用戶Id=?
  • 多Id過濾,如用戶Id IN (?,?,?)
  • 該Id不出現

然后進一步統計,假設共有500個SQL訪問訂單庫,3個過濾字段出現情況如下:

圖片描述

結論明顯,應該選擇用戶Id進行分庫。

等一等,這只是靜態分析,每個SQL訪問的次數是不一樣的,因此還要分析每個SQL的訪問量。我們分析了Top15執行最多的SQL (它們占總執行次數85%),如果按照用戶Id分庫,這些SQL 85%落到單個數據庫, 13%落到多個數據庫,只有2%需要遍歷所有數據庫,明顯優於使用其他Id進行分庫。

通過量化分析,我們知道按照用戶Id分庫是最優的,同時也大致知道分庫對現有系統的影響,比如這個例子中,85%的SQL會落到單個數據庫,這部分的訪問性能會優化,堅定了各方對分庫的信心。

分庫策略

分庫維度確定后,如何把記錄分到各個庫里呢?一般有兩種方式:

  • 根據數值范圍,比如用戶Id為1-9999的記錄分到第一個庫,10000-20000的分到第二個庫,以此類推。
  • 根據數值取模,比如用戶Id mod n,余數為0的記錄放到第一個庫,余數為1的放到第二個庫,以此類推。

兩種分法的優劣比較如下:

圖片描述

實踐中,為了處理簡單,選擇mod分庫的比較多。同時二次分庫時,為了數據遷移方便,一般是按倍數增加,比如初始4個庫,二次分裂為8個,再16個。這樣對於某個庫的數據,一半數據移到新庫,剩余不動,對比每次只增加一個庫,所有數據都要大規模變動。

補充下,mod分庫一般每個庫記錄數比較均勻,但也有些數據庫,存在超級Id,這些Id的記錄遠遠超過其他Id,比如在廣告場景下,某個大廣告主的廣告數可能占總體很大比例。如果按照廣告主Id取模分庫,某些庫的記錄數會特別多,對於這些超級Id,需要提供單獨庫來存儲記錄。

分庫數量

分庫數量首先和單庫能處理的記錄數有關,一般來說,Mysql 單庫超過5000萬條記錄,Oracle單庫超過1億條記錄,DB壓力就很大(當然處理能力和字段數量/訪問模式/記錄長度有進一步關系)。

在滿足上述前提下,如果分庫數量少,達不到分散存儲和減輕DB性能壓力的目的;如果分庫的數量多,好處是每個庫記錄少,單庫訪問性能好,但對於跨多個庫的訪問,應用程序需要訪問多個庫,如果是並發模式,要消耗寶貴的線程資源;如果是串行模式,執行時間會急劇增加。

最后分庫數量還直接影響硬件的投入,一般每個分庫跑在單獨物理機上,多一個庫意味多一台設備。所以具體分多少個庫,要綜合評估,一般初次分庫建議分4-8個庫。

路由透明

分庫從某種意義上來說,意味着DB schema改變了,必然影響應用,但這種改變和業務無關,所以要盡量保證分庫對應用代碼透明,分庫邏輯盡量在數據訪問層處理。當然完全做到這一點很困難,具體哪些應該由DAL負責,哪些由應用負責,這里有一些建議:

1)對於單庫訪問,比如查詢條件指定用戶Id,則該SQL只需訪問特定庫。此時應該由DAL層自動路由到特定庫,當庫二次分裂時,也只要修改mod 因子,應用代碼不受影響。 
2)對於簡單的多庫查詢,DAL負責匯總各個數據庫返回的記錄,此時仍對上層應用透明。 
3)對於帶聚合運算的多庫查詢,如帶groupBy/orderby/min/max/avg等關鍵字,建議DAL匯總單個庫返回的結果,上層應用做進一步處理。一方面DAL全面支持各種case,實現很復雜;另一方面,從1號店實踐來看,這樣的例子不多,在上層應用作針對性處理,更加靈活。

DAL可進一步細分為JDBC和DAL兩層,基於JDBC層面實現分庫路由,系統開發難度大,靈活性低,目前也沒有很好的成功案例;一般是基於持久層框架進一步封裝成DDAL(分布式數據訪問層),實現分庫路由,1號店DAL即基於iBatis進行上層封裝而來。

分頁處理

分庫后,有些分頁查詢需要遍歷所有庫,這些case是分庫最大的受害者。

舉個分頁的例子,比如要求按時間順序展示某個商家的訂單,每頁100條記錄,由於是按商家查詢,需要遍歷所有數據庫,假設庫數量是8,我們來看下分頁處理邏輯:

1)如果取第1頁數據,則需要從每個庫里按時間順序取前100條記錄,8個庫匯總后有800條,然后對這800條記錄在應用里進行二次排序,最后取前100條。 
2)如果取第10頁數據,則需要從每個庫里取前1000(100*10)條記錄,匯總后有8000條記錄,然后對這8000條記錄二次排序后取(900,1000)條記錄。

分庫情況下,對於第k頁記錄,每個庫要多取100*(k-1)條記錄,所有庫加起來,多取的記錄更多,所以越是靠后的分頁,系統要耗費更多內存和執行時間。 
對比沒分庫的情況,無論取那一頁,都只要從單個DB里取100條記錄,而且無需在應用內部做二次排序,非常簡單。

那如何解決分庫情況下的分頁問題呢?有以下幾種辦法:

1)如果是在前台應用提供分頁,則限定用戶只能看前面n頁,這個限制在業務上也是合理的,一般看后面的分頁意義不大(如果一定要看,可以要求用戶縮小范圍重新查詢)。 
2)如果是后台批處理任務要求分批獲取數據,則可以加大page size,比如每次獲取5000條記錄,有效減少分頁數(當然離線訪問一般走備庫,避免沖擊主庫)。 
3)分庫設計時,一般還有配套大數據平台匯總所有分庫的記錄,有些分頁查詢可以考慮走大數據平台。

Lookup映射

分庫字段只有一個,比如這里是用戶Id,但訂單表還有其他字段可唯一區分記錄,比如訂單Id,給定一個訂單Id,相應記錄一定在某個庫里。如果盲目地查詢所有分庫,則帶來不必要的開銷,Lookup映射可根據訂單Id,找到相應的用戶Id,從而實現單庫定位。

可以事先檢索所有訂單Id和用戶Id,保存在Lookup表里,Lookup表的記錄數和訂單庫記錄總數相等,但它只有2個字段,所以存儲和查詢性能都不是問題。實際使用時,一般通過分布式緩存來優化Lookup性能。對於新增的訂單,除了寫訂單表,同時要寫Lookup表。

1)上層應用通過訂單服務/分庫代理和DAL訪問數據庫。 
2)代理對訂單服務實現功能透明,包括聚合運算,非用戶Id到用戶Id的映射。 
3)Lookup表用於訂單Id/用戶Id映射,保證按訂單Id訪問時,可以直接落到單個庫,Cache是Lookup的內存數據映像,提升性能,cache故障時,直接訪問Lookup表。 
4)DAL提供庫的路由,根據用戶Id定位到某個庫,對於多庫訪問,DAL支持可選的並發訪問模式,並支持簡單記錄匯總。 
5)Lookup表初始化數據來自於現有分庫數據,新增記錄時,直接由代理異步寫入。

 


免責聲明!

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



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