一、概述
分表是個(gè)目前算是比較炒的比較流行的概念,特別是在大負(fù)載的情況下,分表是一個(gè)良好分散數(shù)據(jù)庫(kù)壓力的好方法。
首先要了解為什么要分表,分表的好處是什么。我們先來(lái)大概了解以下一個(gè)數(shù)據(jù)庫(kù)執(zhí)行SQL的過(guò)程:
接收到SQL --> 放入SQL執(zhí)行隊(duì)列 --> 使用分析器分解SQL --> 按照分析結(jié)果進(jìn)行數(shù)據(jù)的提取或者修改 --> 返回處理結(jié)果
當(dāng)然,這個(gè)流程圖不一定正確,這只是我自己主觀意識(shí)上這么我認(rèn)為。那么這個(gè)處理過(guò)程當(dāng)中,最容易出現(xiàn)問(wèn)題的是什么?就是說(shuō),如果前一個(gè)SQL沒(méi)
有執(zhí)行完畢的話,后面的SQL是不會(huì)執(zhí)行的,因?yàn)闉榱吮WC數(shù)據(jù)的完整性,必須對(duì)數(shù)據(jù)表文件進(jìn)行鎖定,包括共享鎖和獨(dú)享鎖兩種鎖定。共享鎖是在鎖定的期間,
其它線程也可以訪問(wèn)這個(gè)數(shù)據(jù)文件,但是不允許修改操作,相應(yīng)的,獨(dú)享鎖就是整個(gè)文件就是歸一個(gè)線程所有,其它線程無(wú)法訪問(wèn)這個(gè)數(shù)據(jù)文件。一般MySQL中
最快的存儲(chǔ)引擎MyISAM,它是基于表鎖定的,就是說(shuō)如果一鎖定的話,那么整個(gè)數(shù)據(jù)文件外部都無(wú)法訪問(wèn),必須等前一個(gè)操作完成后,才能接收下一個(gè)操作,
那么在這個(gè)前一個(gè)操作沒(méi)有執(zhí)行完成,后一個(gè)操作等待在隊(duì)列里無(wú)法執(zhí)行的情況叫做阻塞,一般我們通俗意義上叫做“鎖表”。
鎖表直接導(dǎo)致的后果是什么?就是大量的SQL無(wú)法立即執(zhí)行,必須等隊(duì)列前面的SQL全部執(zhí)行完畢才能繼續(xù)執(zhí)行。這個(gè)無(wú)法執(zhí)行的SQL就會(huì)導(dǎo)致沒(méi)有結(jié)果,或者延遲嚴(yán)重,影響用戶體驗(yàn)。
特別是對(duì)于一些使用比較頻繁的表,比如SNS系統(tǒng)中的用戶信息表、論壇系統(tǒng)中的帖子表等等,都是訪問(wèn)量大很大的表,為了保證數(shù)據(jù)的快速提取返回給用戶,必須使用一些處理方式來(lái)解決這個(gè)問(wèn)題,這個(gè)就是我今天要聊到的分表技術(shù)。
分表技術(shù)顧名思義,就是把若干個(gè)存儲(chǔ)相同類型數(shù)據(jù)的表分成幾個(gè)表分表存儲(chǔ),在提取數(shù)據(jù)的時(shí)候,不同的用戶訪問(wèn)不同的表,互不沖突,減少鎖表的幾
率。比如,目前保存用戶分表有兩個(gè)表,一個(gè)是user_1表,還有一個(gè)是 user_2 表,兩個(gè)表保存了不同的用戶信息,user_1
保存了前10萬(wàn)的用戶信息,user_2保存了后10萬(wàn)名用戶的信息,現(xiàn)在如果同時(shí)查詢用戶 heiyeluren1 和 heiyeluren2
這個(gè)兩個(gè)用戶,那么就是分表從不同的表提取出來(lái),減少鎖表的可能。
我下面要講述的兩種分表方法我自己都沒(méi)有實(shí)驗(yàn)過(guò),不保證準(zhǔn)確能用,只是提供一個(gè)設(shè)計(jì)思路。下面關(guān)于分表的例子我假設(shè)是在一個(gè)貼吧系統(tǒng)的基礎(chǔ)上來(lái)進(jìn)行處理和構(gòu)建的。(如果沒(méi)有用過(guò)貼吧的用戶趕緊Google一下)
二、基于基礎(chǔ)表的分表處理
這個(gè)基于基礎(chǔ)表的分表處理方式大致的思想就是:一個(gè)主要表,保存了所有的基本信息,如果某個(gè)項(xiàng)目需要找到它所存儲(chǔ)的表,那么必須從這個(gè)基礎(chǔ)表中
查找出對(duì)應(yīng)的表名等項(xiàng)目,好直接訪問(wèn)這個(gè)表。如果覺得這個(gè)基礎(chǔ)表速度不夠快,可以完全把整個(gè)基礎(chǔ)表保存在緩存或者內(nèi)存中,方便有效的查詢。
我們基于貼吧的情況,構(gòu)建假設(shè)如下的3張表:
1. 貼吧版塊表: 保存貼吧中版塊的信息
2. 貼吧主題表:保存貼吧中版塊中的主題信息,用于瀏覽
3. 貼吧回復(fù)表:保存主題的原始內(nèi)容和回復(fù)內(nèi)容
“貼吧版塊表”包含如下字段:
版塊ID board_id int(10)
版塊名稱 board_name char(50)
子表ID table_id smallint(5)
產(chǎn)生時(shí)間 created datetime
“貼吧主題表”包含如下字段:
主題ID topic_id int(10)
主題名稱 topic_name char(255)
版塊ID board_id int(10)
創(chuàng)建時(shí)間 created datetime
“貼吧回復(fù)表”的字段如下:
回復(fù)ID reply_id int(10)
回復(fù)內(nèi)容 reply_text text
主題ID topic_id int(10)
版塊ID board_id int(10)
創(chuàng)建時(shí)間 created datetime
那么上面保存了我們整個(gè)貼吧中的表結(jié)構(gòu)信息,三個(gè)表對(duì)應(yīng)的關(guān)系是:
版塊 --> 多個(gè)主題
主題 --> 多個(gè)回復(fù)
那么就是說(shuō),表文件大小的關(guān)系是:
版塊表文件 < 主題表文件 < 回復(fù)表文件
所以基本可以確定需要對(duì)主題表和回復(fù)表進(jìn)行分表,已增加我們數(shù)據(jù)檢索查詢更改時(shí)候的速度和性能。
看了上面的表結(jié)構(gòu),會(huì)明顯發(fā)現(xiàn),在“版塊表”中保存了一個(gè)"table_id"字段,這個(gè)字段就是用于保存一個(gè)版塊對(duì)應(yīng)的主題和回復(fù)都是分表保存在什么表里的。
比如我們有一個(gè)叫做“PHP”的貼吧,board_id是1,子表ID也是1,那么這條記錄就是:
board_id | board_name | table_id | created
1 | PHP | 1 | 2007-01-19 00:30:12
相應(yīng)的,如果我需要提取“PHP”吧里的所有主題,那么就必須按照表里保存的table_id來(lái)組合一個(gè)存儲(chǔ)了主題的表名稱,比如我們主題表的前綴是“topic_”,那么組合出來(lái)“PHP”吧對(duì)應(yīng)的主題表應(yīng)該是:“topic_1”,那么我們執(zhí)行:
Select * FROM topic_1 Where board_id = 1 orDER BY topic_id DESC LIMIT 10
這樣就能夠獲取這個(gè)主題下面回復(fù)列表,方便我們進(jìn)行查看,如果需要查看某個(gè)主題下面的回復(fù),我們可以繼續(xù)使用版塊表中保存的“table_id”來(lái)進(jìn)行查詢。比如我們回復(fù)表的前綴是“reply_”,那么就可以組合出“PHP”吧的ID為1的主題的回復(fù):
Select * FROM reply_1 Where topic_id = 1 orDER BY reply_id DESC LIMIT 10
這里,我們能夠清晰的看到,其實(shí)我們這里使用了基礎(chǔ)表,基礎(chǔ)表就是我們的版塊表。那么相應(yīng)的,肯定會(huì)說(shuō):基礎(chǔ)表的數(shù)據(jù)量大了以后如何保證它的速度和效率?
當(dāng)然,我們就必須使得這個(gè)基礎(chǔ)表保持最好的速度和性能,比如,可以采用MySQL的內(nèi)存表來(lái)存儲(chǔ),或者保存在內(nèi)存當(dāng)中,比如Memcache之類的內(nèi)存緩存等等,可以按照實(shí)際情況來(lái)進(jìn)行調(diào)整。
一般基于基礎(chǔ)表的分表機(jī)制在SNS、交友、論壇等Web2.0網(wǎng)站中是個(gè)比較不錯(cuò)的解決方案,在這些網(wǎng)站中,完全可以單獨(dú)使用一個(gè)表來(lái)來(lái)保存基本標(biāo)識(shí)和目標(biāo)表之間的關(guān)系。使用表保存對(duì)應(yīng)關(guān)系的好處是以后擴(kuò)展非常方便,只需要增加一個(gè)表記錄。
【 優(yōu)勢(shì) 】增加刪除節(jié)點(diǎn)非常方便,為后期升級(jí)維護(hù)帶來(lái)很大便利
【 劣勢(shì) 】需要增加表或者對(duì)某一個(gè)表進(jìn)行操作,還是無(wú)法離開數(shù)據(jù)庫(kù),會(huì)產(chǎn)生瓶頸
三、基于 Hash 算法的分表處理
我們知道Hash表就是通過(guò)某個(gè)特殊的Hash算法計(jì)算出的一個(gè)值,這個(gè)值必須是惟一的,并且能夠使用這個(gè)計(jì)算出來(lái)的值查找到需要的值,這個(gè)叫做哈希表。
我們?cè)诜直砝锏膆ash算法跟這個(gè)思想類似:通過(guò)一個(gè)原始目標(biāo)的ID或者名稱通過(guò)一定的hash算法計(jì)算出數(shù)據(jù)存儲(chǔ)表的表名,然后訪問(wèn)相應(yīng)的表。
繼續(xù)拿上面的貼吧來(lái)說(shuō),每個(gè)貼吧有版塊名稱和版塊ID,那么這兩項(xiàng)值是固定的,并且是惟一的,那么我們就可以考慮通過(guò)對(duì)這兩項(xiàng)值中的一項(xiàng)進(jìn)行一些運(yùn)算得出一個(gè)目標(biāo)表的名稱。
現(xiàn)在假如我們針對(duì)我們這個(gè)貼吧系統(tǒng),假設(shè)系統(tǒng)最大允許1億條數(shù)據(jù),考慮每個(gè)表保存100萬(wàn)條記錄,那么整個(gè)系統(tǒng)就不超過(guò)100個(gè)表就能夠容納。按照這個(gè)標(biāo)準(zhǔn),我們假設(shè)在貼吧的版塊ID上進(jìn)行hash,獲得一個(gè)key值,這個(gè)值就是我們的表名,然后訪問(wèn)相應(yīng)的表。
我們構(gòu)造一個(gè)簡(jiǎn)單的hash算法:
function get_hash($id){
$str = bin2hex($id);
$hash = substr($str, 0, 4);
if (strlen($hash)<4){
$hash = str_pad($hash, 4, "0");
}
return $hash;
}
算法大致就是傳入一個(gè)版塊ID值,然后函數(shù)返回一個(gè)4位的字符串,如果字符串長(zhǎng)度不夠,使用0進(jìn)行補(bǔ)全。
比如:get_hash(1),輸出的結(jié)果是“3100”,輸入:get_hash(23819),得到的結(jié)果是:3233,那么我們經(jīng)過(guò)簡(jiǎn)單的跟表前綴組合,就能夠訪問(wèn)這個(gè)表了。那么我們需要訪問(wèn)ID為1的內(nèi)容時(shí)候哦,組合的表將是:topic_3100、reply_3100,那么就可以直接對(duì)目標(biāo)表進(jìn)行訪問(wèn)了。
當(dāng)然,使用hash算法后,有部分?jǐn)?shù)據(jù)是可能在同一個(gè)表的,這一點(diǎn)跟hash表不同,hash表是盡量解決沖突,我們這里不需要,當(dāng)然同樣需要預(yù)測(cè)和分析表數(shù)據(jù)可能保存的表名。
如果需要存儲(chǔ)的數(shù)據(jù)更多,同樣的,可以對(duì)版塊的名字進(jìn)行hash操作,比如也是上面的二進(jìn)制轉(zhuǎn)換成十六進(jìn)制,因?yàn)闈h字比數(shù)字和字母要多很多,那么重復(fù)幾率更小,但是可能組合成的表就更多了,相應(yīng)就必須考慮一些其它的問(wèn)題。
歸根結(jié)底,使用hash方式的話必須選擇一個(gè)好的hash算法,才能生成更多的表,然數(shù)據(jù)查詢的更迅速。
【 優(yōu)點(diǎn)hash算法直接得出目標(biāo)表名稱,效率很高 】通過(guò)
【 劣勢(shì) 】擴(kuò)展性比較差,選擇了一個(gè)hash算法,定義了多少數(shù)據(jù)量,以后只能在這個(gè)數(shù)據(jù)量上跑,不能超過(guò)過(guò)這個(gè)數(shù)據(jù)量,可擴(kuò)展性稍差
四、其它問(wèn)題
1. 搜索問(wèn)題
現(xiàn)在我們已經(jīng)進(jìn)行分表了,那么就無(wú)法直接對(duì)表進(jìn)行搜索,因?yàn)槟銦o(wú)法對(duì)可能系統(tǒng)中已經(jīng)存在的幾十或者幾百個(gè)表進(jìn)行檢索,所以搜索必須借助第三方的組件來(lái)進(jìn)行,比如Lucene作為站內(nèi)搜索引擎是個(gè)不錯(cuò)的選擇。
2. 表文件問(wèn)題
我們知道MySQL的MyISAM引擎每個(gè)表都會(huì)生成三個(gè)文件,*.frm、*.MYD、*.MYI
三個(gè)文件,分表用來(lái)保存表結(jié)構(gòu)、表數(shù)據(jù)和表索引。Linux下面每個(gè)目錄下的文件數(shù)量最好不要超過(guò)1000個(gè),不然檢索數(shù)據(jù)將更慢,那么每個(gè)表都會(huì)生成三
個(gè)文件,相應(yīng)的如果分表超過(guò)300個(gè)表,那么將檢索非常慢,所以這時(shí)候就必須再進(jìn)行分,比如在進(jìn)行數(shù)據(jù)庫(kù)的分離。
使用基礎(chǔ)表,我們可以新增加一個(gè)字段,用來(lái)保存這個(gè)表保存在什么數(shù)據(jù)。使用Hash的方式,我們必須截取hash值中第幾位來(lái)作為數(shù)據(jù)庫(kù)的名字。這樣,完好的解決這個(gè)問(wèn)題。
五、總結(jié)
在大負(fù)載應(yīng)用當(dāng)中,數(shù)據(jù)庫(kù)一直是個(gè)很重要的瓶頸,必須要突破,本文講解了兩種分表的方式,希望對(duì)很多人能夠有啟發(fā)的作用。當(dāng)然,本文代碼和設(shè)想沒(méi)有經(jīng)過(guò)任何代碼測(cè)試,所以無(wú)法保證設(shè)計(jì)的完全準(zhǔn)確實(shí)用,具體還是需要讀者在使用過(guò)程當(dāng)中認(rèn)真分析實(shí)施。