原文地址

1、 前言

對於資料庫而言,在日常開發中我們主要的關注點有兩塊,一個是schema的結構設計,另一個就是索引的優化,這兩塊是影響我們最終系統結構和效能的關鍵部分,自然也是我們花費精力最多的部分;

本文主要介紹資料庫設計中的一般原則和優化手段,包括資料庫的一半正規化、反正規化設計、資料切分、資料路由與合併等等

2、 Schema設計的一般性原則

2.1 概述

正規化理論是關係型資料庫設計的黃金法則,它提供了資料結構化的理論基礎,有效地保證了資料的一致性,應該說,關係型資料庫就是在正規化的基礎上才成長起來的。

資料庫的正規化有很多種,但是我們一般常用的只有第一、二、三正規化和BC正規化,這些正規化直接在我們的資料庫schema設計中得到體現,雖然有時我們根本就沒有意識到。

2.2 第一正規化和第二正規化

在關係型資料結構的實體-關係模型中,是允許實體集和關係集的屬性具有某種程度的子結構的,比如多值屬性和組合屬性;而第一正規化則限制了這種存在,他要求所有的欄位都是不可再分的、原子的,否則就違反了第一正規化;第一正規化主要是為了避免資料表結構過於複雜多樣,使得上層操作的可抽象性和資料一致性遭到破壞。

第二正規化簡單的說,則是要求資料庫中的每條記錄都要有其對應的主鍵id存在,這樣要求的主要目的是為了能夠滿足上層業務要求唯一標識每條記錄的需求;其實資料庫管理系統本身也有這種需求,部分資料庫的索引結構就是基於此的,只不過這不是資料正規化(data normal form)應該關心的東西;

2.3 第三正規化

第三正規化要求在在一個實體集中,不能存在一個非主屬性可以作為該實體集中某個子集的候選主鍵,還可以表述為,不同的關係集中不能存在除了主鍵欄位外的其他相同欄位;這兩者是等價的。

簡單的說,第三正規化主要是要求將實體集儘量拆分,將不同的業務單元屬性欄位拆分到不同的表裡,然後通過關係表進行關聯。

第三正規化一定程度上減少了資料的冗餘,降低了資料不一致的風險;通常情況下,大部分的schema都應達到第三正規化的要求。

2.4 BC正規化

BC正規化是在第三正規化的一個子集,它在第三正規化之上做了更強的約束,即實體集中的任何子集都只能依賴於主鍵(注意,不是主屬性,這一點是BC正規化和第三方正規化的差別所在),不能存在一個非主屬性或非主屬性集可以作為某個子集的主鍵。

BC正規化在定義上和第三方是差不多,他最大程度的減少了資料冗餘,不過在實際應用中,二者基本是一樣的,只有在表的主鍵包含多個欄位時,才會產生差異。

3、 反正規化化設計

3.1 資料冗餘

這裡介紹一個很經典的例子。我們所常見的大部分網站都會有會員系統,使用者需要註冊會員賬號,然後登入才可以享受進一步的功能或服務。對於這張會員表,我們不妨命名為user_info表,一般會包括會員id,會員暱稱,真實姓名,登入密碼,性別,教育經歷、工作經歷、電話、電子郵箱、個人簡介、興趣愛好等資訊,其他可能還包括跟該網站會員業務相關的一些欄位(比如積分、經驗值、充值賬戶餘額等),一般的schema設計是這樣的:

表名

user_info

資料量

100w

功能描述

主要儲存使用者的基本資訊,如id、姓名、年齡、聯絡方式等;

欄位名(新增)

資料型別(精度範圍)

空/非空

預設值

欄位含義

id

BIGINT(20)

N

 

使用者id

name

VARCHAR(32)

N

 

姓名

gender

TINYINT(4)

N

 

性別

age

INT(8)

N

 

年齡

tel

VARCHAR(16)

Y

 

聯絡電話

email

VARCHAR(64)

Y

 

電子郵箱

school

VARCHAR(32)

Y

 

就讀學校

company

VARCHAR(32)

Y

 

供職機構

interest

VARCHAR(512)

Y

 

興趣愛好

gmt_create

DATETIME

N

 

記錄建立時間

gmt_modified

DATETIME

N

 

記錄修改時間

這張表結構看起來是沒什麼問題的,而且在初期業務簡單,使用者數量少、訪問量低的情況下也確實都ok的;

但是隨著網站訪問量不斷增大,每天登入的使用者越來越多,user_info表的訪問量越來越大,瓶頸慢慢出現;

我們經過進一步分析發現,在處理使用者登入的過程中,我們需要的操作很簡單,就是根據使用者輸入的會員暱稱查詢相應記錄,進而判斷密碼是否正確;而會員登入後,也僅會顯示使用者的暱稱或真實姓名。

整個過程前後所涉及到的欄位,只有會員id、暱稱、密碼和真實姓名等3、4個,而user_info表中其他絕大部分欄位,諸如教育經歷、興趣愛好等,在這個過程中是根本不需要的,但他們的體積卻比較大,每次使用者登入都要讀取,白白浪費時間和頻寬。

那麼我們為什麼不把這部分欄位單獨拿出來放到一張新的表裡呢?我們不妨建一個login表,裡邊只有會員id、會員暱稱、登入密碼以及會員真實姓名,每次使用者登入,都只讀取這張login表,這樣不但資料庫讀取記錄的時間會縮短,也可以將應用和資料庫之間網路傳輸量降到最低,完全符合我們前一篇文章裡所提到的將結果集縮減到最小的原則。

表名

login

資料量

100w

功能描述

主要儲存使用者的登入資訊;

欄位名(新增)

資料型別(精度範圍)

空/非空

預設值

欄位含義

id

BIGINT(20)

N

 

使用者id

name

VARCHAR(32)

N

 

姓名

password

VARCHAR(128)

N

 

登入密碼的md5值

但是有人不禁要問,這樣不是產生資料冗餘了嗎?是的,同一份資料在user_info表和login表中各存了一份,確實有可能存在不一致的情況,應用程式中每次新增或修改記錄,都可能需要訪問兩張表,這無形中增加寫操作的成本;所以這種優化方式也不是絕對的,要根據具體的場景區別對待,比如在上述這種讀寫比例非常高的場景中,我們這樣處理就是合適的;而在一個同樣讀寫比例比較高,但是對資料一致性要求也非常高的系統中,對資料進行冗餘儲存就需要花費額外的精力去處理可能存在的資料不一致情況。

3.2 去關聯化

Join是我們在資料庫操作時經常會使用的一個關鍵字,其作用就是將兩張表拼接起來,然後過濾出符合條件的記錄;但是在拼接的過程當中,是採用的是笛卡爾積的方式,其原理圖如下:

在MySQL中,Join的實現方式為Nested Loop Join ,主要以驅動表結果集作為基礎資料進行迴圈,有點類似程式語言中的雙層for迴圈巢狀;這種方式實現最最簡單,效能也基本可以接受;其他資料庫還提供Hash Join、Sort Merge Join等Join方式,都是針對不同場景有效能提升,很難簡單的說誰好誰壞。

從笛卡爾積的基本原理上來看,不管採用何種實現演算法,表間join都是非常耗時耗力的操作,尤其是當其中一張表或兩張表的資料量都非常大時更是如此!

所以我們在做schema設計的時候,應該儘量避免join的出現,通過一定的欄位合併和資料冗餘將這種需求降到最低。

3.3 去一致性約束

在傳統應用中,資料一致性是很重要的一點,但是在很多網際網路應用對資料的一致性要求並不是很高;比如說資料庫的外來鍵約束、唯一性約束等等,當記錄增刪時,資料庫都會去檢查相關的條件是否滿足,這些都是需要額外開銷的,有時候其開銷甚至會超過當前操作本身。

在資料庫層去掉這些約束並不意味著系統中就不需要,我們其實可以將這部分約束校驗提前到應用程式中;前面我們提到過,應用程式的擴充套件相對來說是比較容易的,所以我們何嘗不充分利用一下來為資料庫減負呢?

3.4 去SQL

3.4.1 資料庫底層資料儲存

我們使用的關係型資料庫雖然提供了表、欄位、索引、sql等種種特性,但是歸根到底,其最底層的資料儲存仍然是<k,v>格式,比如MySQL,其資料的儲存都是有由下層儲存引擎來負責的,雖然在邏輯結構上我們看到的是一張張表和其中一個個分散的欄位,但是實際上每條記錄在物理儲存上都是一個整體;所謂的表結構、欄位這些只是上層來維護的元資料,對底層來說,其實沒有欄位的概念,就是一條條的物理記錄;

比如說我們前邊的user_info表,我們如果執行類似下邊的一條sql:

select user_id,user_name from user_info where age =8;

雖然我只想返回user_id和user_name兩個欄位,但是儲存引擎還是要將所有滿足的age=8的記錄取出來,然後在分別掃描每條記錄,取出我們需要的兩個欄位資料返回;

所以我們可以看到,所謂的關係資料庫只不過是在<k,v>系統的上層做了進一步的封裝,比如說許可權驗證、sql解析、二級索引等等;

3.4.2 MySQL資料庫的層次結構

MySQL雖然是個開源資料庫,體積也遠小於DB2、Oracle這些商業資料庫,但是一個RDBMS必備的結構MySQL卻一樣也不少,正所謂“麻雀雖小,五臟俱全”。

下邊我們就來看下MySQL到底有怎樣的層次結構;

MySQL從上到下,可以分為三層,第一部分為客戶端,中間部分為資料庫管理系統(DBMS),最底層為儲存引擎層;MySQL使用獨立的儲存引擎,可以方便切換,這一點和其他資料庫有所不同;

3.4.3 哪些是可以不要的

前面給出了MySQL資料庫的層次結構,其他資料庫結構也是類似的;那麼不妨從上到下好好想一想,整個體系中哪些東西其實不是我們必需的?

首先,Client端肯定是需要的,我們總要連線資料庫的,那麼來看第二層DBMS層,其中的連線管理是需要的,不管什麼樣的資料庫總要處理客戶端連線;如果一個數據庫在安全網路環境中,並且只有我們自己在用的話,那麼就不需要使用者許可權驗證了,這一層可以去掉;為了減輕資料庫負擔,我們也不使用sql,需要的邏輯可以直接使用API在應用層實現,那麼這一層也可以省掉;而對於訪問控制模組,因為資料庫為我們自己所獨享,或者都是可信任的使用者,所以這一層也不那麼重要了,或者說是可以進一步簡化的;

我們再來往下看儲存引擎層。索引的話可以很好的提高資料的查詢效率,這一點不管在什麼型別的儲存系統中都適用,那麼這個功能我們需要保留,但是如果我們只想要個<k,v>儲存的話,那就可以只保留主鍵索引好了;事務管理呢,最多我們不用好了,不麻煩資料庫了;鎖的話即使我們自己使用也會有很多的併發訪問控制,那麼需要保留;

經過這樣的精簡之後,我們的資料庫還剩以下這些東西;

3.4.4 NoSQL儲存系統

前面我們提到了去關聯化、去一致性約束以及資料冗餘等等;3.1中我們討論了關係型資料庫的本質,其實就是在<k,v>儲存之上又封裝和增添了諸多的功能,而3.3中我們又對關係型資料庫進行了裁剪,去掉了一些我們不必需的;經過所有的這麼步驟,剩下的部分(圖1-1所示),基本上就是現在NoSQL儲存了;

所以,NoSQL不是什麼高階的東東,而是關係型資料庫做了退化,迴歸到了其基礎本源;而分散式的特性,也並不是NoSQL獨有的,關係型資料之所以難有分散式的架構,本質就是因為其選擇了“向上生長”,上層的複雜特性制約了其“橫向”生長的能力;而NoSQL只不過是在<k,v>之上,選擇了另外一個生長方向而已。

由於去掉了上層的“高階”特性,NoSQL系統的效能有了比較大的提升,同時由於橫向生長,其儲存能力也有了很大的增強;

所以當我們的系統受制於關係型資料庫的效能時,不妨放棄schema模式,來嘗試一下“自由”的NoSQL資料庫。

4、 資料擴充套件

4.1 Scale Up和Scale Out

Scale up主要是指增強資料庫的單機處理能力,比如說提高CPU、記憶體、硬碟、網絡卡等硬體配置;其實scaleup這個概念包括我們後便將要提到的scale out,不光對於資料庫,對於大部分的軟體系統都適用的,只不過具體的實施方案會有所差異;

因為scale up主要是增加機器硬體配置,相對來說比較簡單,也不需要遷移資料,對於技術人員來說沒有新的東西,所以對於中小型系統來說非常合適;

scale out是指橫向的擴充套件,就是增加資料庫例項或節點來增加整體的處理能力,這裡邊還包括兩種方式,一種常見的資料複製,比如MySQL的Replication,Oracle rac等;另一種橫向擴充套件的方式是進行資料切分,也就是說把本應該放在一臺機器上的資料切成幾部分,分別放在不同的節點上(並不是相同資料的備份),這樣訪問的壓力就會分散到多個節點。

scale out的優點是成本低,如果整個系統都使用PC Server的話,可以用很低的成本來支撐海量的資料和高併發;而且一般來說,這種擴充套件是線性的,即有多少機器,就能支撐多少的資料和多大的訪問量,但通常這需要有個比較好的資料系統架構或中介軟體系統來支援;

以淘寶的交易庫來說,原來使用的都是IOE(I指的是IBM的小型機,O指Oracle資料庫,E即EMC的高階儲存),採用主備雙機方式,用Orcle和高階儲存來支撐每天的巨大訪問量,但是整個系統的成本也非常高(據說一套下來要2000多萬),而經過去IOE以後,通過使用MySQL和廉價的PC Server(線上16主16備,雙11的時候擴充套件為32主32備),通過資料切分和Replication機制,不僅整個資料庫的線上處理能力提高了4倍,成本也降為原來的1 / 8不到,同時資料的安全性和容災能力也得到了保證;

但是資料庫技術不是本文關注的重點,下邊將主要介紹和我們日常開發聯絡更為緊密的資料切分知識。

4.2 資料切分

4.2.1 為什麼要切分資料

對於這個問題,可能有人會覺得的是廢話:你前邊不是已經說過了嗎,幹嘛還問?沒錯,擴充套件系統,支援更大訪問和併發和替換商業資料庫,降低成本這兩個確實我們進行資料切分的主要原因;但這樣來說太籠統了

對於資料庫來講,不管是商業的還是開源的,其單庫和單表的承載能力都是有限的;在通常的業務場景下(寫操作和讀操作比較均衡),普通的pc server上,MySQL資料庫單表資料記錄的承載能力在千萬級(資料量在TB級別)左右,TPS大概在千這個級別(具體測試環境和資料可參考另一篇文件);當然,我們在這裡沒有必要苛求具體的資料,因為這和具體業務場景、實際讀寫比、伺服器硬體配置、具體的資料引擎、MySQL的配置引數等相關,比如說,如果只是將MySQL作為日誌資料庫(基本只有寫操作,不需要建索引),單表的支撐能力可達到上億甚至是十億的級別;但這畢竟只是種極限場景,不能拿來用作一般場景的參考。

另外需要說明的是,前面說的單庫單表的承載能力有限,並不是說當資料量超過這個上限時,資料庫就會馬上崩潰或者拒絕服務,而是在這種情況下,資料庫的整體的讀寫效能就會急劇下降,甚至於一條很簡單的sql查詢也可能會超時,如果本來就是負載很重的一張表,那與崩潰無異了!

4.2.2 資料切分基本原則

資料切分所要遵循的原則主要有兩點:

第一就是將資料均衡分散在多個處理節點上,其實這裡主要強調的是均衡,但這個均衡並不可簡單的看成是每個節點上庫(表)數量相等或是記錄條數一樣;而更多是要從資料訪問和處理能力的均衡上去考慮,主要原則有以下幾條。

a、 不同節點業務關聯度要低

b、 同一節點業務型別儘量一致

c、 資料(訪問量)要均衡

d、 資料的一致性和安全性

4.2.3 垂直切分

垂直切分主要是根據表中資料的業務型別,將不同業務的資料放在不同的表或者資料庫中;

在系統結構設計中我們會經常提到一個原則,叫做高內聚、低耦合,其實這個原則在資料庫的垂直切分中同樣適用;所以在做水平切分的時候要儘量做到不同功能的表關聯儘可能少,這不但可以減少SQL語句中的join出現的機率,同時後續表結構變更時也更容易;

對於中小型系統來講,很多人喜歡各種複雜功能一個sql搞定,甚至有些還使用儲存過程,這樣對前端程式來講確實方便了許多;但是這種方便也往往會給我們帶來傷害,尤其是當資料量和訪問量都增長比較快的情況下,你會發現幾條慢查詢可能讓你的整個系統直接失去響應!

4.2.4 水平切分

4.2.4.1 什麼是水平切分

垂直切分主要是按照系統功能來切分的,所以同樣是有瓶頸存在的,比如說,某一項功能比其他的要複雜許多,或者資料量要大很多,很難再進一步拆分,這樣就不適合垂直切分了;這在實際的業務場景中是存在的;

比如說淘寶的訂單系統,雖然功能看起來簡單,但是由於交易型別複雜,中間狀態繁多,耦合性非常強,加之訂單數量巨大,其系統是很難再進行功能上的剝離的;但是這個系統屬於底層基礎系統,為了支撐巨大的訪問量,只能採取水平資料切分方式來解決高併發問題。

水平切分就是我們通常所說的分庫分表,主要是將一張表中的資料按照某個欄位(比如說使用者id、商品id、訂單id等)分散儲存在多張結構相同的表中,這樣訪問的壓力就會分散到多張表上;

在平時的開發中,資料的水平切分比垂直切分應用的更多,因為一般來講,需要進行垂直資料切分時,通常系統的規模和負載都已經很大了(尤其是使用oracle的時候),這時候我們最先實施的,往往是通過RPC或者服務化將應用分成多個系統,底層資料表之間的依賴,很自然的會轉化成系統間的介面依賴,所以這個時候,資料庫當然也會跟著分開了,不需要太刻意其考慮垂直切分這個概念了。

4.2.4.2 水平切分優點

成本固定;只要在系統設計之初就指定好分表數量和分表字段,不管是要分成8個庫1024張表,還是16個庫4096張表,其成本都是一樣的;

解決了單表瓶頸問題;水平切分方式很好的解決了垂直切分時可能存在的單表瓶頸問題,只要在開始時做好容量預估,設定適當的切分數量,基本可以滿足業務很長一段時期內的儲存需求;

對事務透明;分表對於資料庫來說是透明的,所以原來的事務該怎麼做還怎麼做。

4.2.4.3 水平切分缺點

水平切分的雖然很有效,但是其缺點也不少,主要如下:

sql路由變得複雜;每次在做了切分的庫或表上執行sql時,都必須要明確指出目標分庫或分表;這無形中增加業務方的成本。

分表字段單一;水平切分只能使用單一的分表字段;如果業務中有需求按照非分表字段進行查詢,則會變得很困難,只能掃描所有的表;一個解決方案就是,按照不同查詢欄位做多份分表;但是又要花費精力去解決資料冗餘問題。

join操作變得困難;顯而易見,以前單表間的join放到多表上是無法執行的,這時候我們最好還是選擇放棄;

二次擴充套件比較麻煩;如果分表之後,我們資料增長太快,又達到儲存瓶頸了,就面臨著二次拆分的命運;但因為路由規則發生了改變,遷移資料的麻煩是避免不了的;所以要有必要的手段去保證遷移資料時系統依然能夠對外提供服務。

4.2.4.4 水平切分注意事項

在做水平切分後,我們的部分業務實現方式或是開發方式可能需要隨著改變;以下是我們再做水平切分時需要注意的點,主要是針對水平切分的弱點而言的:

根據業務場景確定切分欄位;業務中根據什麼欄位去查詢,就用什麼欄位去分表;

避免熱點資料問題;通常切分時採用的hash演算法理論上可以保證資料的分散性,但在實際應用中,仍可能遇到資料熱點問題;理論是理論,實際歸實際,沒有絕對的,不要以為分了表就萬事大吉了。

分表宜多不宜少;這樣做主要是為了儘量避免後期可能遇到的二次拆分,因為前面我們說過,拆成1024張表和拆成4096張表的操作成本是一樣的。

避免分表上的join操作;在分表的缺點中我們就提到過,join在水平切分場景下會很困難,所以在業務實現中,對這種情況能避免就避免,哪怕犧牲一些簡潔性,多繞幾步。

避免非分表字段查詢;道理也是一樣的,切分後只能按照切分欄位進行查詢;如果非要按其他欄位查詢,那就冗餘資料吧。

4.2.5 其他切分方式

上邊我們提到的是我們最常見的切分方式,其他還有一些切分方式不太“規矩”,它們具有部分水平切分或垂直切分的特點,但又很難直接歸入到某一分類中;正如那句老話所言:山無定勢,水無定形。

4.2.5.1 邏輯切分

邏輯切分類似於垂直切分,也是將資料按照不同業務邏輯拆分到不同的表中;但這種方式追求的不是單純的負載均衡,而是不同業務的資料隔離;比如說某部分資料讀多些少,某部分資料則可能讀少寫多;這樣進行隔離後,可以充分的利用不同的業務特點進行優化,比如說建立不同的索引結構,使用不同的查詢方案等等;前面我們提到的資料冗餘其實就屬於這類切分方式。

另一個典型的案例就是資料傾斜;比如說對於淘寶上的賣家來說,有些賣家比較小,可能他的店鋪中的寶貝只有幾十數百個,但是有些品牌賣家或熱門店鋪,他們的寶貝可能有上萬甚至是數十萬,再加上未上架的或是已下架的歷史寶貝,數量會更多!這兩類使用者的處理,如果使用相同的邏輯,很可能會產生問題;但是如果我們將這部分大賣家提出來放在另外的節點上來處理,效果可能會好很多。

4.2.5.2 時間切分

時間的切分主要是將記錄按照建立時間的先後順序,放在不同的表中;mysql的分割槽表便提供了這樣一種切分的機制;分割槽表對外邏輯上表現為一張表,但是實際物理儲存上是多張表,不同的表對應不同的時間區段;使用者可以設定建立新分割槽表的週期,比如說一天或者是一週,在某個時間點插入的記錄便會寫入對應的分割槽表;讀取時,會從最近一個分割槽開始掃描,直到找到目標記錄。

4.2.5.3 冷熱切分

冷熱切分主要是按照資料的訪問頻率對資料進行隔離,有點類似於前邊我們提到的邏輯切分。

淘寶的交易歷史庫就是典型的代表。淘寶的訂單總量巨大,每天產生的訂單數量也非常多;如果我們為使用者提供訂單查詢服務,那麼巨大的訂單量會使我們的服務效能下降很多;但是實際中我們會發現,使用者很少會去查詢自己三個月以前的訂單,那我們不妨將使用者三個月以前的訂單拿出來單獨提供儲存和查詢服務,這樣就可以使使用者訪問頻繁的訂單表資料量變得很小,從而可以提供更高的處理效能。

4.2.5.1 體積切分

按體積切分主要是按照資料表的尺寸或是記錄條數進行切分,這種切分一般適用於業務型別單一,對錶的體積可以很好預估的情況,主要是為了避免表的尺寸過大而是效能下降。

一般來說,只有日誌型別的表適用於這種切分方式,通常是以自增id作為判斷標識,因為基本不存在刪除和修改操作,所以可以很好的控制體積;不過這種情況下大部分系統更傾向於使用按時間切分的方式,所以按體積切分的實際應用很少。

4.3 資料路由與合併

當我們進行了分庫分表之後,一個我們不得不面對的問題就是sql的路由。當我們將資料分散儲存在諸如名為test_0000、test_0001的分表中時,我們會發現,必須要對原來的程式程式碼或資料庫進行相應的改造,否則程式將找不到正確的庫或表;這種情況下,通常的解決方案有三種:

4.3.1 修改程式

這種方案,只需將程式裡涉及到資料庫讀寫的程式碼按照分表邏輯進行改造即可,技術上比較簡單,不需要額外的軟體或者是技術的支援;缺點是對業務程式碼侵入性強,可能涉及到多個地方的修改,工作量較大,而且後期的修改和維護成本也比較高。

4.3.2 修改資料庫

這種方法對程式透明,是的上層業務邏輯不需要考慮分庫分表的讀寫規則,應用程式碼可以保持不變;

缺點是需要修改資料庫系統,或者以模組、外掛等形式對資料庫進行增強,開發成本和後期維護成本都很高,部分商業資料庫根本無法自行修改。

4.3.3 使用中間層代理

這個應該是目前採用最多的方式;其優點是對上層業務邏輯和底層資料庫都透明,只需要對應用的訪問層進行簡單改造,即可快速切換到拆分之後的資料庫,不管是開發人員還是資料庫管理人員都不需要增加多少工作成本;

其缺點是技術門檻較高,需要專門的人員來開發和維護;功能受限,部分在單庫單表下的常見操作在這種中間層代理的方式下會變得麻煩,比如說跨庫、跨表join,全域性資料分組與排序等。

對於以上幾種資料路由方案,不同的場景可能會有不同的選擇,具體要看自身的業務需求,技術儲備以及實施成本等。

 

4.4 Scale up之快閃記憶體儲存

以上通篇幾乎都在介紹如何對資料庫進行水平擴充套件和資料切分,其實有時候我們不必搞得如此複雜,如果僅通過簡單的硬體升級就能滿足業務增長對資料庫的要求,我們何樂而不為呢?

我們都知道,普通的伺服器,甚至是很多高階儲存,都是基於機械硬碟存放資料的;而機械硬碟最大的缺點就是速度上存在物理上限;

一般的機械硬碟讀寫資料都要經過尋道操作,主要由兩個步驟組成:一是移動磁頭,二是轉動碟片;這兩者都屬於機械操作,前者一般在2ms~4ms,而後者,對於現在常用的SATA盤或SAS盤來說,一般會在1ms以內,所以,一次尋道操作要耗費大約5ms左右的時間,而對於一些老式硬碟來說,這個時間可能要達到10ms甚至更多!也就是說,單塊磁碟隨機讀寫的iops只有100左右,這簡直低的不可忍受,再牛B的系統也被拖死了!

對於一個初級團隊,再沒有很多技術積累的情況下,盲目的進行水平擴容往往會給自己帶來更高的維護成本和更差的系統穩定性,所以這個時候我們不妨嘗試一下資料庫效能提升的又一利器——快閃記憶體儲存;畢竟我們不斷對系統進行優化的目標就是為了獲得更高的效能!

1、 前言

對於資料庫而言,在日常開發中我們主要的關注點有兩塊,一個是schema的結構設計,另一個就是索引的優化,這兩塊是影響我們最終系統結構和效能的關鍵部分,自然也是我們花費精力最多的部分;

本文主要介紹資料庫設計中的一般原則和優化手段,包括資料庫的一半正規化、反正規化設計、資料切分、資料路由與合併等等

2、 Schema設計的一般性原則

2.1 概述

正規化理論是關係型資料庫設計的黃金法則,它提供了資料結構化的理論基礎,有效地保證了資料的一致性,應該說,關係型資料庫就是在正規化的基礎上才成長起來的。

資料庫的正規化有很多種,但是我們一般常用的只有第一、二、三正規化和BC正規化,這些正規化直接在我們的資料庫schema設計中得到體現,雖然有時我們根本就沒有意識到。

2.2 第一正規化和第二正規化

在關係型資料結構的實體-關係模型中,是允許實體集和關係集的屬性具有某種程度的子結構的,比如多值屬性和組合屬性;而第一正規化則限制了這種存在,他要求所有的欄位都是不可再分的、原子的,否則就違反了第一正規化;第一正規化主要是為了避免資料表結構過於複雜多樣,使得上層操作的可抽象性和資料一致性遭到破壞。

第二正規化簡單的說,則是要求資料庫中的每條記錄都要有其對應的主鍵id存在,這樣要求的主要目的是為了能夠滿足上層業務要求唯一標識每條記錄的需求;其實資料庫管理系統本身也有這種需求,部分資料庫的索引結構就是基於此的,只不過這不是資料正規化(data normal form)應該關心的東西;

2.3 第三正規化

第三正規化要求在在一個實體集中,不能存在一個非主屬性可以作為該實體集中某個子集的候選主鍵,還可以表述為,不同的關係集中不能存在除了主鍵欄位外的其他相同欄位;這兩者是等價的。

簡單的說,第三正規化主要是要求將實體集儘量拆分,將不同的業務單元屬性欄位拆分到不同的表裡,然後通過關係表進行關聯。

第三正規化一定程度上減少了資料的冗餘,降低了資料不一致的風險;通常情況下,大部分的schema都應達到第三正規化的要求。

2.4 BC正規化

BC正規化是在第三正規化的一個子集,它在第三正規化之上做了更強的約束,即實體集中的任何子集都只能依賴於主鍵(注意,不是主屬性,這一點是BC正規化和第三方正規化的差別所在),不能存在一個非主屬性或非主屬性集可以作為某個子集的主鍵。

BC正規化在定義上和第三方是差不多,他最大程度的減少了資料冗餘,不過在實際應用中,二者基本是一樣的,只有在表的主鍵包含多個欄位時,才會產生差異。

3、 反正規化化設計

3.1 資料冗餘

這裡介紹一個很經典的例子。我們所常見的大部分網站都會有會員系統,使用者需要註冊會員賬號,然後登入才可以享受進一步的功能或服務。對於這張會員表,我們不妨命名為user_info表,一般會包括會員id,會員暱稱,真實姓名,登入密碼,性別,教育經歷、工作經歷、電話、電子郵箱、個人簡介、興趣愛好等資訊,其他可能還包括跟該網站會員業務相關的一些欄位(比如積分、經驗值、充值賬戶餘額等),一般的schema設計是這樣的:

表名

user_info

資料量

100w

功能描述

主要儲存使用者的基本資訊,如id、姓名、年齡、聯絡方式等;

欄位名(新增)

資料型別(精度範圍)

空/非空

預設值

欄位含義

id

BIGINT(20)

N

 

使用者id

name

VARCHAR(32)

N

 

姓名

gender

TINYINT(4)

N

 

性別

age

INT(8)

N

 

年齡

tel

VARCHAR(16)

Y

 

聯絡電話

email

VARCHAR(64)

Y

 

電子郵箱

school

VARCHAR(32)

Y

 

就讀學校

company

VARCHAR(32)

Y

 

供職機構

interest

VARCHAR(512)

Y

 

興趣愛好

gmt_create

DATETIME

N

 

記錄建立時間

gmt_modified

DATETIME

N

 

記錄修改時間

這張表結構看起來是沒什麼問題的,而且在初期業務簡單,使用者數量少、訪問量低的情況下也確實都ok的;

但是隨著網站訪問量不斷增大,每天登入的使用者越來越多,user_info表的訪問量越來越大,瓶頸慢慢出現;

我們經過進一步分析發現,在處理使用者登入的過程中,我們需要的操作很簡單,就是根據使用者輸入的會員暱稱查詢相應記錄,進而判斷密碼是否正確;而會員登入後,也僅會顯示使用者的暱稱或真實姓名。

整個過程前後所涉及到的欄位,只有會員id、暱稱、密碼和真實姓名等3、4個,而user_info表中其他絕大部分欄位,諸如教育經歷、興趣愛好等,在這個過程中是根本不需要的,但他們的體積卻比較大,每次使用者登入都要讀取,白白浪費時間和頻寬。

那麼我們為什麼不把這部分欄位單獨拿出來放到一張新的表裡呢?我們不妨建一個login表,裡邊只有會員id、會員暱稱、登入密碼以及會員真實姓名,每次使用者登入,都只讀取這張login表,這樣不但資料庫讀取記錄的時間會縮短,也可以將應用和資料庫之間網路傳輸量降到最低,完全符合我們前一篇文章裡所提到的將結果集縮減到最小的原則。

表名

login

資料量

100w

功能描述

主要儲存使用者的登入資訊;

欄位名(新增)

資料型別(精度範圍)

空/非空

預設值

欄位含義

id

BIGINT(20)

N

 

使用者id

name

VARCHAR(32)

N

 

姓名

password

VARCHAR(128)

N

 

登入密碼的md5值

但是有人不禁要問,這樣不是產生資料冗餘了嗎?是的,同一份資料在user_info表和login表中各存了一份,確實有可能存在不一致的情況,應用程式中每次新增或修改記錄,都可能需要訪問兩張表,這無形中增加寫操作的成本;所以這種優化方式也不是絕對的,要根據具體的場景區別對待,比如在上述這種讀寫比例非常高的場景中,我們這樣處理就是合適的;而在一個同樣讀寫比例比較高,但是對資料一致性要求也非常高的系統中,對資料進行冗餘儲存就需要花費額外的精力去處理可能存在的資料不一致情況。

3.2 去關聯化

Join是我們在資料庫操作時經常會使用的一個關鍵字,其作用就是將兩張表拼接起來,然後過濾出符合條件的記錄;但是在拼接的過程當中,是採用的是笛卡爾積的方式,其原理圖如下:

在MySQL中,Join的實現方式為Nested Loop Join ,主要以驅動表結果集作為基礎資料進行迴圈,有點類似程式語言中的雙層for迴圈巢狀;這種方式實現最最簡單,效能也基本可以接受;其他資料庫還提供Hash Join、Sort Merge Join等Join方式,都是針對不同場景有效能提升,很難簡單的說誰好誰壞。

從笛卡爾積的基本原理上來看,不管採用何種實現演算法,表間join都是非常耗時耗力的操作,尤其是當其中一張表或兩張表的資料量都非常大時更是如此!

所以我們在做schema設計的時候,應該儘量避免join的出現,通過一定的欄位合併和資料冗餘將這種需求降到最低。

3.3 去一致性約束

在傳統應用中,資料一致性是很重要的一點,但是在很多網際網路應用對資料的一致性要求並不是很高;比如說資料庫的外來鍵約束、唯一性約束等等,當記錄增刪時,資料庫都會去檢查相關的條件是否滿足,這些都是需要額外開銷的,有時候其開銷甚至會超過當前操作本身。

在資料庫層去掉這些約束並不意味著系統中就不需要,我們其實可以將這部分約束校驗提前到應用程式中;前面我們提到過,應用程式的擴充套件相對來說是比較容易的,所以我們何嘗不充分利用一下來為資料庫減負呢?

3.4 去SQL

3.4.1 資料庫底層資料儲存

我們使用的關係型資料庫雖然提供了表、欄位、索引、sql等種種特性,但是歸根到底,其最底層的資料儲存仍然是<k,v>格式,比如MySQL,其資料的儲存都是有由下層儲存引擎來負責的,雖然在邏輯結構上我們看到的是一張張表和其中一個個分散的欄位,但是實際上每條記錄在物理儲存上都是一個整體;所謂的表結構、欄位這些只是上層來維護的元資料,對底層來說,其實沒有欄位的概念,就是一條條的物理記錄;

比如說我們前邊的user_info表,我們如果執行類似下邊的一條sql:

select user_id,user_name from user_info where age =8;

雖然我只想返回user_id和user_name兩個欄位,但是儲存引擎還是要將所有滿足的age=8的記錄取出來,然後在分別掃描每條記錄,取出我們需要的兩個欄位資料返回;

所以我們可以看到,所謂的關係資料庫只不過是在<k,v>系統的上層做了進一步的封裝,比如說許可權驗證、sql解析、二級索引等等;

3.4.2 MySQL資料庫的層次結構

MySQL雖然是個開源資料庫,體積也遠小於DB2、Oracle這些商業資料庫,但是一個RDBMS必備的結構MySQL卻一樣也不少,正所謂“麻雀雖小,五臟俱全”。

下邊我們就來看下MySQL到底有怎樣的層次結構;

MySQL從上到下,可以分為三層,第一部分為客戶端,中間部分為資料庫管理系統(DBMS),最底層為儲存引擎層;MySQL使用獨立的儲存引擎,可以方便切換,這一點和其他資料庫有所不同;

3.4.3 哪些是可以不要的

前面給出了MySQL資料庫的層次結構,其他資料庫結構也是類似的;那麼不妨從上到下好好想一想,整個體系中哪些東西其實不是我們必需的?

首先,Client端肯定是需要的,我們總要連線資料庫的,那麼來看第二層DBMS層,其中的連線管理是需要的,不管什麼樣的資料庫總要處理客戶端連線;如果一個數據庫在安全網路環境中,並且只有我們自己在用的話,那麼就不需要使用者許可權驗證了,這一層可以去掉;為了減輕資料庫負擔,我們也不使用sql,需要的邏輯可以直接使用API在應用層實現,那麼這一層也可以省掉;而對於訪問控制模組,因為資料庫為我們自己所獨享,或者都是可信任的使用者,所以這一層也不那麼重要了,或者說是可以進一步簡化的;

我們再來往下看儲存引擎層。索引的話可以很好的提高資料的查詢效率,這一點不管在什麼型別的儲存系統中都適用,那麼這個功能我們需要保留,但是如果我們只想要個<k,v>儲存的話,那就可以只保留主鍵索引好了;事務管理呢,最多我們不用好了,不麻煩資料庫了;鎖的話即使我們自己使用也會有很多的併發訪問控制,那麼需要保留;

經過這樣的精簡之後,我們的資料庫還剩以下這些東西;

3.4.4 NoSQL儲存系統

前面我們提到了去關聯化、去一致性約束以及資料冗餘等等;3.1中我們討論了關係型資料庫的本質,其實就是在<k,v>儲存之上又封裝和增添了諸多的功能,而3.3中我們又對關係型資料庫進行了裁剪,去掉了一些我們不必需的;經過所有的這麼步驟,剩下的部分(圖1-1所示),基本上就是現在NoSQL儲存了;

所以,NoSQL不是什麼高階的東東,而是關係型資料庫做了退化,迴歸到了其基礎本源;而分散式的特性,也並不是NoSQL獨有的,關係型資料之所以難有分散式的架構,本質就是因為其選擇了“向上生長”,上層的複雜特性制約了其“橫向”生長的能力;而NoSQL只不過是在<k,v>之上,選擇了另外一個生長方向而已。

由於去掉了上層的“高階”特性,NoSQL系統的效能有了比較大的提升,同時由於橫向生長,其儲存能力也有了很大的增強;

所以當我們的系統受制於關係型資料庫的效能時,不妨放棄schema模式,來嘗試一下“自由”的NoSQL資料庫。

4、 資料擴充套件

4.1 Scale Up和Scale Out

Scale up主要是指增強資料庫的單機處理能力,比如說提高CPU、記憶體、硬碟、網絡卡等硬體配置;其實scaleup這個概念包括我們後便將要提到的scale out,不光對於資料庫,對於大部分的軟體系統都適用的,只不過具體的實施方案會有所差異;

因為scale up主要是增加機器硬體配置,相對來說比較簡單,也不需要遷移資料,對於技術人員來說沒有新的東西,所以對於中小型系統來說非常合適;

scale out是指橫向的擴充套件,就是增加資料庫例項或節點來增加整體的處理能力,這裡邊還包括兩種方式,一種常見的資料複製,比如MySQL的Replication,Oracle rac等;另一種橫向擴充套件的方式是進行資料切分,也就是說把本應該放在一臺機器上的資料切成幾部分,分別放在不同的節點上(並不是相同資料的備份),這樣訪問的壓力就會分散到多個節點。

scale out的優點是成本低,如果整個系統都使用PC Server的話,可以用很低的成本來支撐海量的資料和高併發;而且一般來說,這種擴充套件是線性的,即有多少機器,就能支撐多少的資料和多大的訪問量,但通常這需要有個比較好的資料系統架構或中介軟體系統來支援;

以淘寶的交易庫來說,原來使用的都是IOE(I指的是IBM的小型機,O指Oracle資料庫,E即EMC的高階儲存),採用主備雙機方式,用Orcle和高階儲存來支撐每天的巨大訪問量,但是整個系統的成本也非常高(據說一套下來要2000多萬),而經過去IOE以後,通過使用MySQL和廉價的PC Server(線上16主16備,雙11的時候擴充套件為32主32備),通過資料切分和Replication機制,不僅整個資料庫的線上處理能力提高了4倍,成本也降為原來的1 / 8不到,同時資料的安全性和容災能力也得到了保證;

但是資料庫技術不是本文關注的重點,下邊將主要介紹和我們日常開發聯絡更為緊密的資料切分知識。

4.2 資料切分

4.2.1 為什麼要切分資料

對於這個問題,可能有人會覺得的是廢話:你前邊不是已經說過了嗎,幹嘛還問?沒錯,擴充套件系統,支援更大訪問和併發和替換商業資料庫,降低成本這兩個確實我們進行資料切分的主要原因;但這樣來說太籠統了

對於資料庫來講,不管是商業的還是開源的,其單庫和單表的承載能力都是有限的;在通常的業務場景下(寫操作和讀操作比較均衡),普通的pc server上,MySQL資料庫單表資料記錄的承載能力在千萬級(資料量在TB級別)左右,TPS大概在千這個級別(具體測試環境和資料可參考另一篇文件);當然,我們在這裡沒有必要苛求具體的資料,因為這和具體業務場景、實際讀寫比、伺服器硬體配置、具體的資料引擎、MySQL的配置引數等相關,比如說,如果只是將MySQL作為日誌資料庫(基本只有寫操作,不需要建索引),單表的支撐能力可達到上億甚至是十億的級別;但這畢竟只是種極限場景,不能拿來用作一般場景的參考。

另外需要說明的是,前面說的單庫單表的承載能力有限,並不是說當資料量超過這個上限時,資料庫就會馬上崩潰或者拒絕服務,而是在這種情況下,資料庫的整體的讀寫效能就會急劇下降,甚至於一條很簡單的sql查詢也可能會超時,如果本來就是負載很重的一張表,那與崩潰無異了!

4.2.2 資料切分基本原則

資料切分所要遵循的原則主要有兩點:

第一就是將資料均衡分散在多個處理節點上,其實這裡主要強調的是均衡,但這個均衡並不可簡單的看成是每個節點上庫(表)數量相等或是記錄條數一樣;而更多是要從資料訪問和處理能力的均衡上去考慮,主要原則有以下幾條。

a、不同節點業務關聯度要低

b、同一節點業務型別儘量一致

c、資料(訪問量)要均衡

d、資料的一致性和安全性

4.2.3 垂直切分

垂直切分主要是根據表中資料的業務型別,將不同業務的資料放在不同的表或者資料庫中;

在系統結構設計中我們會經常提到一個原則,叫做高內聚、低耦合,其實這個原則在資料庫的垂直切分中同樣適用;所以在做水平切分的時候要儘量做到不同功能的表關聯儘可能少,這不但可以減少SQL語句中的join出現的機率,同時後續表結構變更時也更容易;

對於中小型系統來講,很多人喜歡各種複雜功能一個sql搞定,甚至有些還使用儲存過程,這樣對前端程式來講確實方便了許多;但是這種方便也往往會給我們帶來傷害,尤其是當資料量和訪問量都增長比較快的情況下,你會發現幾條慢查詢可能讓你的整個系統直接失去響應!

4.2.4 水平切分

4.2.4.1 什麼是水平切分

垂直切分主要是按照系統功能來切分的,所以同樣是有瓶頸存在的,比如說,某一項功能比其他的要複雜許多,或者資料量要大很多,很難再進一步拆分,這樣就不適合垂直切分了;這在實際的業務場景中是存在的;

比如說淘寶的訂單系統,雖然功能看起來簡單,但是由於交易型別複雜,中間狀態繁多,耦合性非常強,加之訂單數量巨大,其系統是很難再進行功能上的剝離的;但是這個系統屬於底層基礎系統,為了支撐巨大的訪問量,只能採取水平資料切分方式來解決高併發問題。

水平切分就是我們通常所說的分庫分表,主要是將一張表中的資料按照某個欄位(比如說使用者id、商品id、訂單id等)分散儲存在多張結構相同的表中,這樣訪問的壓力就會分散到多張表上;

在平時的開發中,資料的水平切分比垂直切分應用的更多,因為一般來講,需要進行垂直資料切分時,通常系統的規模和負載都已經很大了(尤其是使用oracle的時候),這時候我們最先實施的,往往是通過RPC或者服務化將應用分成多個系統,底層資料表之間的依賴,很自然的會轉化成系統間的介面依賴,所以這個時候,資料庫當然也會跟著分開了,不需要太刻意其考慮垂直切分這個概念了。

4.2.4.2 水平切分優點

成本固定;只要在系統設計之初就指定好分表數量和分表字段,不管是要分成8個庫1024張表,還是16個庫4096張表,其成本都是一樣的;

解決了單表瓶頸問題;水平切分方式很好的解決了垂直切分時可能存在的單表瓶頸問題,只要在開始時做好容量預估,設定適當的切分數量,基本可以滿足業務很長一段時期內的儲存需求;

對事務透明;分表對於資料庫來說是透明的,所以原來的事務該怎麼做還怎麼做。

4.2.4.3 水平切分缺點

水平切分的雖然很有效,但是其缺點也不少,主要如下:

sql路由變得複雜;每次在做了切分的庫或表上執行sql時,都必須要明確指出目標分庫或分表;這無形中增加業務方的成本。

分表字段單一;水平切分只能使用單一的分表字段;如果業務中有需求按照非分表字段進行查詢,則會變得很困難,只能掃描所有的表;一個解決方案就是,按照不同查詢欄位做多份分表;但是又要花費精力去解決資料冗餘問題。

join操作變得困難;顯而易見,以前單表間的join放到多表上是無法執行的,這時候我們最好還是選擇放棄;

二次擴充套件比較麻煩;如果分表之後,我們資料增長太快,又達到儲存瓶頸了,就面臨著二次拆分的命運;但因為路由規則發生了改變,遷移資料的麻煩是避免不了的;所以要有必要的手段去保證遷移資料時系統依然能夠對外提供服務。

4.2.4.4 水平切分注意事項

在做水平切分後,我們的部分業務實現方式或是開發方式可能需要隨著改變;以下是我們再做水平切分時需要注意的點,主要是針對水平切分的弱點而言的:

根據業務場景確定切分欄位;業務中根據什麼欄位去查詢,就用什麼欄位去分表;

避免熱點資料問題;通常切分時採用的hash演算法理論上可以保證資料的分散性,但在實際應用中,仍可能遇到資料熱點問題;理論是理論,實際歸實際,沒有絕對的,不要以為分了表就萬事大吉了。

分表宜多不宜少;這樣做主要是為了儘量避免後期可能遇到的二次拆分,因為前面我們說過,拆成1024張表和拆成4096張表的操作成本是一樣的。

避免分表上的join操作;在分表的缺點中我們就提到過,join在水平切分場景下會很困難,所以在業務實現中,對這種情況能避免就避免,哪怕犧牲一些簡潔性,多繞幾步。

避免非分表字段查詢;道理也是一樣的,切分後只能按照切分欄位進行查詢;如果非要按其他欄位查詢,那就冗餘資料吧。

4.2.5 其他切分方式

上邊我們提到的是我們最常見的切分方式,其他還有一些切分方式不太“規矩”,它們具有部分水平切分或垂直切分的特點,但又很難直接歸入到某一分類中;正如那句老話所言:山無定勢,水無定形。

4.2.5.1 邏輯切分

邏輯切分類似於垂直切分,也是將資料按照不同業務邏輯拆分到不同的表中;但這種方式追求的不是單純的負載均衡,而是不同業務的資料隔離;比如說某部分資料讀多些少,某部分資料則可能讀少寫多;這樣進行隔離後,可以充分的利用不同的業務特點進行優化,比如說建立不同的索引結構,使用不同的查詢方案等等;前面我們提到的資料冗餘其實就屬於這類切分方式。

另一個典型的案例就是資料傾斜;比如說對於淘寶上的賣家來說,有些賣家比較小,可能他的店鋪中的寶貝只有幾十數百個,但是有些品牌賣家或熱門店鋪,他們的寶貝可能有上萬甚至是數十萬,再加上未上架的或是已下架的歷史寶貝,數量會更多!這兩類使用者的處理,如果使用相同的邏輯,很可能會產生問題;但是如果我們將這部分大賣家提出來放在另外的節點上來處理,效果可能會好很多。

4.2.5.2 時間切分

時間的切分主要是將記錄按照建立時間的先後順序,放在不同的表中;mysql的分割槽表便提供了這樣一種切分的機制;分割槽表對外邏輯上表現為一張表,但是實際物理儲存上是多張表,不同的表對應不同的時間區段;使用者可以設定建立新分割槽表的週期,比如說一天或者是一週,在某個時間點插入的記錄便會寫入對應的分割槽表;讀取時,會從最近一個分割槽開始掃描,直到找到目標記錄。

4.2.5.3 冷熱切分

冷熱切分主要是按照資料的訪問頻率對資料進行隔離,有點類似於前邊我們提到的邏輯切分。

淘寶的交易歷史庫就是典型的代表。淘寶的訂單總量巨大,每天產生的訂單數量也非常多;如果我們為使用者提供訂單查詢服務,那麼巨大的訂單量會使我們的服務效能下降很多;但是實際中我們會發現,使用者很少會去查詢自己三個月以前的訂單,那我們不妨將使用者三個月以前的訂單拿出來單獨提供儲存和查詢服務,這樣就可以使使用者訪問頻繁的訂單表資料量變得很小,從而可以提供更高的處理效能。

4.2.5.1 體積切分

按體積切分主要是按照資料表的尺寸或是記錄條數進行切分,這種切分一般適用於業務型別單一,對錶的體積可以很好預估的情況,主要是為了避免表的尺寸過大而是效能下降。

一般來說,只有日誌型別的表適用於這種切分方式,通常是以自增id作為判斷標識,因為基本不存在刪除和修改操作,所以可以很好的控制體積;不過這種情況下大部分系統更傾向於使用按時間切分的方式,所以按體積切分的實際應用很少。

4.3 資料路由與合併

當我們進行了分庫分表之後,一個我們不得不面對的問題就是sql的路由。當我們將資料分散儲存在諸如名為test_0000、test_0001的分表中時,我們會發現,必須要對原來的程式程式碼或資料庫進行相應的改造,否則程式將找不到正確的庫或表;這種情況下,通常的解決方案有三種:

4.3.1 修改程式

這種方案,只需將程式裡涉及到資料庫讀寫的程式碼按照分表邏輯進行改造即可,技術上比較簡單,不需要額外的軟體或者是技術的支援;缺點是對業務程式碼侵入性強,可能涉及到多個地方的修改,工作量較大,而且後期的修改和維護成本也比較高。

4.3.2 修改資料庫

這種方法對程式透明,是的上層業務邏輯不需要考慮分庫分表的讀寫規則,應用程式碼可以保持不變;

缺點是需要修改資料庫系統,或者以模組、外掛等形式對資料庫進行增強,開發成本和後期維護成本都很高,部分商業資料庫根本無法自行修改。

4.3.3 使用中間層代理

這個應該是目前採用最多的方式;其優點是對上層業務邏輯和底層資料庫都透明,只需要對應用的訪問層進行簡單改造,即可快速切換到拆分之後的資料庫,不管是開發人員還是資料庫管理人員都不需要增加多少工作成本;

其缺點是技術門檻較高,需要專門的人員來開發和維護;功能受限,部分在單庫單表下的常見操作在這種中間層代理的方式下會變得麻煩,比如說跨庫、跨表join,全域性資料分組與排序等。

對於以上幾種資料路由方案,不同的場景可能會有不同的選擇,具體要看自身的業務需求,技術儲備以及實施成本等。

 

4.4 Scale up之快閃記憶體儲存

以上通篇幾乎都在介紹如何對資料庫進行水平擴充套件和資料切分,其實有時候我們不必搞得如此複雜,如果僅通過簡單的硬體升級就能滿足業務增長對資料庫的要求,我們何樂而不為呢?

我們都知道,普通的伺服器,甚至是很多高階儲存,都是基於機械硬碟存放資料的;而機械硬碟最大的缺點就是速度上存在物理上限;

一般的機械硬碟讀寫資料都要經過尋道操作,主要由兩個步驟組成:一是移動磁頭,二是轉動碟片;這兩者都屬於機械操作,前者一般在2ms~4ms,而後者,對於現在常用的SATA盤或SAS盤來說,一般會在1ms以內,所以,一次尋道操作要耗費大約5ms左右的時間,而對於一些老式硬碟來說,這個時間可能要達到10ms甚至更多!也就是說,單塊磁碟隨機讀寫的iops只有100左右,這簡直低的不可忍受,再牛B的系統也被拖死了!

對於一個初級團隊,再沒有很多技術積累的情況下,盲目的進行水平擴容往往會給自己帶來更高的維護成本和更差的系統穩定性,所以這個時候我們不妨嘗試一下資料庫效能提升的又一利器——快閃記憶體儲存;畢竟我們不斷對系統進行優化的目標就是為了獲得更高的效能!