1 基本思想之什么是分庫(kù)分表?
創(chuàng)新互聯(lián)公司2013年至今,先為甌海等服務(wù)建站,甌海等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為甌海企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。
從字面上簡(jiǎn)單理解,就是把原本存儲(chǔ)于一個(gè)庫(kù)的數(shù)據(jù)分塊存儲(chǔ)到多個(gè)庫(kù)上,把原本存儲(chǔ)于一個(gè)表的數(shù)據(jù)分塊存儲(chǔ)到多個(gè)表上。
2 基本思想之為什么要分庫(kù)分表?
數(shù)據(jù)庫(kù)中的數(shù)據(jù)量不一定是可控的,在未進(jìn)行分庫(kù)分表的情況下,隨著時(shí)間和業(yè)務(wù)的發(fā)展,庫(kù)中的表會(huì)越來(lái)越多,表中的數(shù)據(jù)量也會(huì)越來(lái)越大,相應(yīng)地,數(shù)據(jù)操作,增刪改查的開(kāi)銷(xiāo)也會(huì)越來(lái)越大;另外,由于無(wú)法進(jìn)行分布式式部署,而一臺(tái)服務(wù)器的資源(CPU、磁盤(pán)、內(nèi)存、IO等)是有限的,最終數(shù)據(jù)庫(kù)所能承載的數(shù)據(jù)量、數(shù)據(jù)處理能力都將遭遇瓶頸。
3 分庫(kù)分表的實(shí)施策略。
分庫(kù)分表有垂直切分和水平切分兩種。
3.1 何謂垂直切分,即將表按照功能模塊、關(guān)系密切程度劃分出來(lái),部署到不同的庫(kù)上。例如,我們會(huì)建立定義數(shù)據(jù)庫(kù)workDB、商品數(shù)據(jù)庫(kù)payDB、用戶數(shù)據(jù)庫(kù)userDB、日志數(shù)據(jù)庫(kù)logDB等,分別用于存儲(chǔ)項(xiàng)目數(shù)據(jù)定義表、商品定義表、用戶數(shù)據(jù)表、日志數(shù)據(jù)表等。
3.2 何謂水平切分,當(dāng)一個(gè)表中的數(shù)據(jù)量過(guò)大時(shí),我們可以把該表的數(shù)據(jù)按照某種規(guī)則,例如userID散列,進(jìn)行劃分,然后存儲(chǔ)到多個(gè)結(jié)構(gòu)相同的表,和不同的庫(kù)上。例如,我們的userDB中的用戶數(shù)據(jù)表中,每一個(gè)表的數(shù)據(jù)量都很大,就可以把userDB切分為結(jié)構(gòu)相同的多個(gè)userDB:part0DB、part1DB等,再將userDB上的用戶數(shù)據(jù)表userTable,切分為很多userTable:userTable0、userTable1等,然后將這些表按照一定的規(guī)則存儲(chǔ)到多個(gè)userDB上。
3.3 應(yīng)該使用哪一種方式來(lái)實(shí)施數(shù)據(jù)庫(kù)分庫(kù)分表,這要看數(shù)據(jù)庫(kù)中數(shù)據(jù)量的瓶頸所在,并綜合項(xiàng)目的業(yè)務(wù)類(lèi)型進(jìn)行考慮。
如果數(shù)據(jù)庫(kù)是因?yàn)楸硖喽斐珊A繑?shù)據(jù),并且項(xiàng)目的各項(xiàng)業(yè)務(wù)邏輯劃分清晰、低耦合,那么規(guī)則簡(jiǎn)單明了、容易實(shí)施的垂直切分必是首選。
而如果數(shù)據(jù)庫(kù)中的表并不多,但單表的數(shù)據(jù)量很大、或數(shù)據(jù)熱度很高,這種情況之下就應(yīng)該選擇水平切分,水平切分比垂直切分要復(fù)雜一些,它將原本邏輯上屬于一體的數(shù)據(jù)進(jìn)行了物理分割,除了在分割時(shí)要對(duì)分割的粒度做好評(píng)估,考慮數(shù)據(jù)平均和負(fù)載平均,后期也將對(duì)項(xiàng)目人員及應(yīng)用程序產(chǎn)生額外的數(shù)據(jù)管理負(fù)擔(dān)。
在現(xiàn)實(shí)項(xiàng)目中,往往是這兩種情況兼而有之,這就需要做出權(quán)衡,甚至既需要垂直切分,又需要水平切分。我們的游戲項(xiàng)目便綜合使用了垂直與水平切分,我們首先對(duì)數(shù)據(jù)庫(kù)進(jìn)行垂直切分,然后,再針對(duì)一部分表,通常是用戶數(shù)據(jù)表,進(jìn)行水平切分。
4 分庫(kù)分表存在的問(wèn)題。
4.1 事務(wù)問(wèn)題。
在執(zhí)行分庫(kù)分表之后,由于數(shù)據(jù)存儲(chǔ)到了不同的庫(kù)上,數(shù)據(jù)庫(kù)事務(wù)管理出現(xiàn)了困難。如果依賴數(shù)據(jù)庫(kù)本身的分布式事務(wù)管理功能去執(zhí)行事務(wù),將付出高昂的性能代價(jià);如果由應(yīng)用程序去協(xié)助控制,形成程序邏輯上的事務(wù),又會(huì)造成編程方面的負(fù)擔(dān)。
4.2 跨庫(kù)跨表的join問(wèn)題。
在執(zhí)行了分庫(kù)分表之后,難以避免會(huì)將原本邏輯關(guān)聯(lián)性很強(qiáng)的數(shù)據(jù)劃分到不同的表、不同的庫(kù)上,這時(shí),表的關(guān)聯(lián)操作將受到限制,我們無(wú)法join位于不同分庫(kù)的表,也無(wú)法join分表粒度不同的表,結(jié)果原本一次查詢能夠完成的業(yè)務(wù),可能需要多次查詢才能完成。
4.3 額外的數(shù)據(jù)管理負(fù)擔(dān)和數(shù)據(jù)運(yùn)算壓力。
額外的數(shù)據(jù)管理負(fù)擔(dān),最顯而易見(jiàn)的就是數(shù)據(jù)的定位問(wèn)題和數(shù)據(jù)的增刪改查的重復(fù)執(zhí)行問(wèn)題,這些都可以通過(guò)應(yīng)用程序解決,但必然引起額外的邏輯運(yùn)算,例如,對(duì)于一個(gè)記錄用戶成績(jī)的用戶數(shù)據(jù)表userTable,業(yè)務(wù)要求查出成績(jī)最好的100位,在進(jìn)行分表之前,只需一個(gè)order by語(yǔ)句就可以搞定,但是在進(jìn)行分表之后,將需要n個(gè)order by語(yǔ)句,分別查出每一個(gè)分表的前100名用戶數(shù)據(jù),然后再對(duì)這些數(shù)據(jù)進(jìn)行合并計(jì)算,才能得出結(jié)果。
大數(shù)據(jù)量?那是數(shù)據(jù)庫(kù)的架構(gòu)設(shè)計(jì)的問(wèn)題了,跟PHP框架,甚至跟語(yǔ)言都沒(méi)有關(guān)系。
要考慮數(shù)據(jù)庫(kù)的分表分庫(kù),數(shù)據(jù)庫(kù)服務(wù)器的集群等操作
我前幾天有一個(gè)面試,面試題就是有這樣一道題。先把自己的思路說(shuō)一下,因?yàn)樾畔⒘糠浅5拇?,所以我采用了分表,分?4張表,每個(gè)小時(shí)一張,雖然凌晨時(shí)刻的表可能很少數(shù)據(jù),但這樣sum字段的問(wèn)題就容易解決了,我理解的sum字段是一個(gè)小時(shí)同一個(gè)用戶在相同的環(huán)境的登陸次數(shù)。這樣理解不知對(duì)否,請(qǐng)網(wǎng)友自行甄辨。然后我通過(guò)PHP中的fgets函數(shù)一行一行的數(shù)據(jù)取出,入表。實(shí)驗(yàn)了幾萬(wàn)條數(shù)據(jù)是沒(méi)有問(wèn)題的,但是上億條數(shù)據(jù)可能夠嗆。這一點(diǎn)也請(qǐng)網(wǎng)友注意,我也是新手。只是看到這里沒(méi)有答案,給大家一個(gè)參考。廢話不多,看流程:
日志文件(access.log)格式:
200?/alipeng.gif?zoneid=2bannerid=44clentid=6materialid=64redirect=http%3a%2f%2f;time=1384444800.832ip=127.0.0.1user_agent=Mozilla/5.0?(X11;?Linux?x86_64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/31.0.1650.48?Safari/537.36utrace=a6dbdd2f6a37b946165b7ae98dcd4f79
502?/alipeng.gif?zoneid=2bannerid=44clentid=6materialid=64redirect=http%3a%2f%2f;time=1384444800.904ip=127.0.0.1user_agent=Mozilla/5.0?(X11;?Linux?x86_64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/31.0.1650.48?Safari/537.36utrace=a6dbdd2f6a37b946165b7ae98dcd4f79
配置文件cfg.php:
define(HOST,'localhost');//主機(jī)名
define(USER,'root');//數(shù)據(jù)庫(kù)賬號(hào)
define(PASS,'111111');//數(shù)據(jù)庫(kù)密碼
define(DBNAME,'test');//所用的數(shù)據(jù)庫(kù)
define(CHARSET,'utf8');//使用的字符集
具體代碼test.php:
?php
header("content-type:text/html;charset=utf-8");
require?'./cfg.php';
$link?=?mysql_connect(HOST,USER,PASS)?or?die('連接數(shù)據(jù)庫(kù)失敗');
//程序中自動(dòng)建庫(kù)和建表,這樣一定程度上拖慢了程序的速度
//創(chuàng)建數(shù)據(jù)庫(kù)
$crdb="create?database?if?not?exists?".DBNAME;
if(!mysql_query($crdb)){
die('創(chuàng)建數(shù)據(jù)庫(kù)失敗');
}
//鏈接數(shù)據(jù)庫(kù)
mysql_select_db(DBNAME)?or?die('選擇數(shù)據(jù)庫(kù)失敗');
mysql_set_charset(CHARSET);
//因?yàn)閿?shù)據(jù)量很大我將數(shù)據(jù)按小時(shí)分表,分成24個(gè)表,每小時(shí)一個(gè)表,這樣num字段的值也好做統(tǒng)計(jì)
//數(shù)據(jù)循環(huán)建表
for($i=0;$i24;$i++){
if($i10){
$tbhz='0'.$i;//如果前10張表,表后綴應(yīng)該是00-09
}else{
$tbhz=$i;
}
$ctbsql="create?table?if?not?exists?logininfo_{$tbhz}(
id?int?not?null?auto_increment?primary?key,
zoneid?int?not?null?default?0,
bannerid?int?not?null?default?0,
clentid?int?not?null?default?0,
materialid?int?not?null?default?0,
redirect?char(200)?not?null?default?'',
time?char(16)?not?null?default?'',
user_agent?char(200)?not?null?default?'',
utrace?char(32)?not?null?default?'',
sum?int?not?null?default?0
)TYPE=MyISAM?DEFAULT?CHARACTER?SET?utf8?COLLATE?utf8_general_ci";
mysql_query($ctbsql);
}
//打開(kāi)文件
$file=fopen("./access.log",'r')?or?die("打開(kāi)文件失敗");
//對(duì)文件內(nèi)容進(jìn)行循環(huán),直到文件末尾才停止
while?(!feof($file)){
//每次讀取一行
$line?=?fgets($file,1024);
//狀態(tài)是200的進(jìn)行寫(xiě)入數(shù)據(jù)庫(kù)操作
if(preg_match('/^200/',$line)){
$pinfo=parse_url($line);//url信息
$ext=$pinfo['query'];//取得傳遞的各個(gè)參數(shù)
$parray=explode('',$ext);//根據(jù)分解為數(shù)組
//因?yàn)榉纸鉃閿?shù)組后并不是要的值,所以要對(duì)值進(jìn)行一次截取,將等號(hào)及等號(hào)左邊的都去掉
foreach($parray?as?$val){
$narray[]=ltrim(strstr($val,'='),'=');
}
$narray[8]=rtrim($narray[8],'_');
//截取時(shí)間的秒數(shù)
$getmun=substr($parray[5],5,10);
$time=date('Y-m-d?H',$getmun);//將秒數(shù)轉(zhuǎn)化為時(shí)間類(lèi)型。
//得到表后綴
$tbhz=date('H',$getmun);
$sql="insert?into?logininfo_{$tbhz}?values(null,'{$narray[0]}','{$narray[1]}','{$narray[2]}','{$narray[3]}','{$narray[4]}','{$time}','{$narray[7]}','{$narray[8]}',0)";
//echo?$sql;
$res=mysql_query($sql);//執(zhí)行插入
if(!$res?||?!mysql_affected_rows()0){
die('寫(xiě)入數(shù)據(jù)庫(kù)失敗');
}
unset($narray);//循環(huán)一次將narray銷(xiāo)毀,為下一次循環(huán)做準(zhǔn)備
//var_dump(parse_url($line)['query']);
}
}
fclose($file);//關(guān)閉
//因?yàn)閟um字段還是0,下面代碼段需要處理sum字段的值
//24張表循環(huán)處理
for($i=0;$i24;$i++){
if($i10){
$tbhz='0'.$i;//如果前10張表,表后綴應(yīng)該是00-09
}else{
$tbhz=$i;
}
//該sql語(yǔ)句是把同一個(gè)小時(shí)內(nèi),并且符合條件相等的登陸的總次數(shù)和需要的登陸信息查出,為下面修改sum做準(zhǔn)備
$sql="SELECT?COUNT('zoneid')?AS?sum,zoneid,bannerid,clentid,materialid,redirect,user_agent,utrace?FROM?logininfo_{$tbhz}?GROUP?BY?zoneid,bannerid,clentid,materialid,redirect,user_agent,utrace";
//發(fā)送查詢sql
$res=mysql_query($sql);
if($res??mysql_num_rows($res)0){
while($row=mysql_fetch_assoc($res)){
//修改sum字段,即同一小時(shí)內(nèi)的登陸次數(shù)
$upsql="update?logininfo_{$tbhz}?set?sum='{$row['sum']}'?where?zoneid='{$row['zoneid']}'?and?bannerid='{$row['bannerid']}'?and?clentid='{$row['clentid']}'?and?materialid='{$row['materialid']}'?and?redirect='{$row['redirect']}'?and?user_agent='{$row['user_agent']}'?and?utrace='{$row['utrace']}'";
//發(fā)送修改sql,執(zhí)行修改sum
$upres=mysql_query($upsql);
if(!$upres){
die('修改登陸sum失敗');
}
}
}
}
echo?'數(shù)據(jù)成功入表';
使用說(shuō)明:
將配置文件cfg.php中的連接數(shù)據(jù)庫(kù)賬號(hào)、密碼修改為自己本機(jī)的(默認(rèn)新增的庫(kù)名是test)
直接運(yùn)行test1.php
用原生php來(lái)寫(xiě),封裝一個(gè)db類(lèi),封裝一個(gè)table類(lèi),通過(guò)工廠模式來(lái)創(chuàng)建某個(gè)庫(kù)某個(gè)表的對(duì)象,有了這個(gè)對(duì)象就可以操作分庫(kù)分表了;這個(gè)中間件可以有自己的命名空間。