為了應(yīng)對(duì)產(chǎn)品海量用戶的愿景需求,這里將設(shè)計(jì)一個(gè)分布式的數(shù)據(jù)庫(kù)存儲(chǔ)方案,以便能滿足數(shù)據(jù)量的驟增、云服務(wù)的橫向擴(kuò)展、后臺(tái)接口開(kāi)發(fā)的兼容性,以及數(shù)據(jù)遷移等問(wèn)題,避免日后因?yàn)槿繑?shù)據(jù)都存放在單臺(tái)服務(wù)器上的限制。
是指將不需要進(jìn)行必要關(guān)聯(lián)查詢的表分開(kāi)存放,如存放事件推送的weili_event_pushto和存放標(biāo)簽的weili_tag;同時(shí),對(duì)于 同一個(gè)表,因?yàn)榇娣诺臄?shù)據(jù)量是可預(yù)見(jiàn)式的暴增,如上述的weili_event_pushto,每時(shí)每刻都會(huì)產(chǎn)生大量的來(lái)自用戶發(fā)布的事件,因此為了突破 MySQL單表的限制以及其他問(wèn)題,需要將此表同時(shí)創(chuàng)建N份。
在上面進(jìn)行了分庫(kù)分表后,開(kāi)發(fā)人員在讀取時(shí),就需要根據(jù)相應(yīng)的規(guī)則找到對(duì)應(yīng) 的數(shù)據(jù)庫(kù)和數(shù)據(jù)庫(kù)表,這里建議每個(gè)表都需要有int(11)類型的id字段,以便作為分表的參考。
在完成了分庫(kù)分表和制定路由規(guī)則后,考慮到日后有數(shù)據(jù)庫(kù)的DB變更,為減少DB變更對(duì)現(xiàn)有數(shù)據(jù)庫(kù)表的影響,這里建議每個(gè)表都增加text類型的extra_data字段,并且使用json格式進(jìn)行轉(zhuǎn)換存儲(chǔ)。
在有了N臺(tái)數(shù)據(jù)庫(kù)服務(wù)器以及每個(gè)表都拆分成M張表后,為減少后臺(tái)接口開(kāi)發(fā)人員的壓力,有必須在后臺(tái)接口框架提供可配置 的支持。即:數(shù)據(jù)庫(kù)的變更不應(yīng)影響開(kāi)發(fā)人員現(xiàn)有的開(kāi)發(fā),也不需要開(kāi)發(fā)人員作出代碼層面的改動(dòng),只需要稍微配置一下即可。關(guān)于這塊,請(qǐng)見(jiàn)下面的框架實(shí)現(xiàn)部 分。
對(duì)于相同表的建表語(yǔ)句,可以通過(guò)腳本來(lái)自動(dòng)生成,然后直接導(dǎo)入數(shù)據(jù)即可。
PhalApi框架主要需要實(shí)現(xiàn)的是路由這一層的映射,并且通過(guò)可配置的方式進(jìn)行控制,同時(shí)還應(yīng)支持生產(chǎn)環(huán)境和測(cè)試環(huán)境的異同,如在測(cè)試環(huán)境我們明顯不需要1000張數(shù)據(jù)庫(kù)的表。為此,需要提供一種 表名 + id 映射到 數(shù)據(jù)庫(kù)服務(wù)器 + 具體哪張表 的規(guī)則。 如上圖所示,表名會(huì)統(tǒng)一加上前綴,并且將id按一定的表總數(shù)進(jìn)行取模,最后再根據(jù)得到的具體表名,通過(guò)映射表查找到對(duì)應(yīng) 的數(shù)據(jù)庫(kù)服務(wù)器進(jìn)行操作。其中,model層為開(kāi)發(fā)實(shí)現(xiàn),數(shù)據(jù)庫(kù)表的映射由接口框架實(shí)現(xiàn)支持。
修改./Config/dbs.php文件,以下是參考的示例配置。其中servers為DB服務(wù)器,包括數(shù)據(jù)庫(kù)的賬號(hào)信息等,tables為數(shù)據(jù)庫(kù)表的映射關(guān)系,其中default下標(biāo)為缺省的數(shù)據(jù)庫(kù)路由。
在每個(gè)數(shù)據(jù)庫(kù)表里面,可以配置多個(gè)數(shù)據(jù)庫(kù)表,通過(guò)開(kāi)始的下標(biāo)start和結(jié)束的下標(biāo)end來(lái)對(duì)表進(jìn)行分布式存放,并且如果沒(méi)有start和end的,則視為不需要拆分存放,同時(shí)也是當(dāng)找不到合適時(shí)的拆分表時(shí)所采用的默認(rèn)配置。
return array(
/**
* avaiable db servers
*/
'servers' => array(
'db_demo' => array(
'host' => 'localhost', //數(shù)據(jù)庫(kù)域名
'name' => 'test', //數(shù)據(jù)庫(kù)名字
'user' => 'root', //數(shù)據(jù)庫(kù)用戶名
'password' => '123456', //數(shù)據(jù)庫(kù)密碼
'port' => '3306', //數(shù)據(jù)庫(kù)端口
),
),
/**
* custom table map
*/
'tables' => array(
'__default__' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_demo'),
),
),
'demo' => array(
'prefix' => 'tbl_',
'key' => 'id',
'map' => array(
array('db' => 'db_demo'),
array('start' => 0, 'end' => 2, 'db' => 'db_demo'),
),
),
),
);
上面示例配置的意思是:
表名 DB服務(wù)器
tbl_demo db_demo
tbl_demo_0 db_demo
tbl_demo_1 db_demo
tbl_demo_2 db_demo
這里說(shuō)的基本SQL語(yǔ)句是指:僅是這個(gè)表所特有的字段,排除已固定公共有的自增主鍵id,和擴(kuò)展字段ext_data。下面是一個(gè)示例:
`name` varchar(11) DEFAULT NULL,
由于拆分后的數(shù)據(jù)庫(kù)表數(shù)量眾多,這里提供了一個(gè)快捷的腳本工具來(lái)生成所需要?jiǎng)?chuàng)建的數(shù)據(jù)庫(kù)表。
$ php ./build_sqls.php
Usage: ./build_sqls.php <table> [engine=InnoDB]
執(zhí)行上面的腳本,輸入數(shù)據(jù)庫(kù)表參數(shù)后:
php ./build_sqls.php demo
將會(huì)從配置文件 里面尋找所需要?jiǎng)?chuàng)建的表,并生成類似以下的SQL語(yǔ)句:
/**
* DB: localhost db_demo
*/
CREATE TABLE `demo` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/**
* DB: localhost db_demo
*/
CREATE TABLE `tpl_demo_0` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `tpl_demo_1` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `tpl_demo_2` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT NULL,
`ext_data` text COMMENT 'json data here',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
在將上面的SQL語(yǔ)句導(dǎo)入數(shù)據(jù)庫(kù)后,即可以像之前那樣操作數(shù)據(jù)庫(kù)。下面是一些示例:
DI()->notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()->config->get('dbs'), true);
DI()->notorm->demo->where('id', '1')->fetch();
用到了拆分表的代碼示例,假設(shè)event表被拆分成了3個(gè)表,則客戶端在調(diào)用里,需要根據(jù)(id % 3 )來(lái)拼接合適的數(shù)據(jù)庫(kù)表名,其他使用不變。
DI()->notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()->config->get('dbs'), true);
$row = DI()->notorm->demo_0->where('id', '3')->fetch();
$row = DI()->notorm->demo_1->where('id', '10')->fetch();
$row = DI()->notorm->demo_2->where('id', '2')->fetch();
更好的寫法,應(yīng)該是繼承于PhalApi_Model_NotORM,并統(tǒng)一實(shí)現(xiàn)分表的操作,如:
<?php
class Model_Demo extends PhalApi_Model_NotORM {
protected function getTableName($id) {
$tableName = 'demo';
if ($id !== null) {
$tableName .= '_' . ($id % 3);
}
return $tableName;
}
}
然后,上面的查詢分別對(duì)應(yīng):
$model = new Model_Demo();
$row = $model->get('3', 'id');
$row = $model->get('10', 'id');
$row = $model->get('2', 'id');
更進(jìn)一步,我們可以通過(guò)$this->getORM($id)來(lái)獲取分表的實(shí)例進(jìn)行分表的操作,如:
<?php
class Model_Demo extends PhalApi_Model_NotORM {
//... ...
public function getNameById($id) {
$row = $this->getORM($id)->select('name')->fetchRow(); //假設(shè)$id為3,則 $this->getORM($id) 等效于 DI()->notorm->demo_0
return !empty($row) ? $row['name'] : '';
}
}
當(dāng)需要使用多個(gè)數(shù)據(jù)庫(kù)時(shí),可以先在servers中可以配置多組數(shù)據(jù)庫(kù)的信息,然后在tables為不同的數(shù)據(jù)庫(kù)表指定不同的數(shù)據(jù)庫(kù)服務(wù)器。
假設(shè)我們有兩臺(tái)數(shù)據(jù)庫(kù)服務(wù)器,分別叫做db_A、db_B,即:
return array(
/**
* DB數(shù)據(jù)庫(kù)服務(wù)器集群
*/
'servers' => array(
'db_A' => array( //db_A
'host' => '192.168.0.1', //數(shù)據(jù)庫(kù)域名
// ... ...
),
'db_B' => array( //db_B
'host' => '192.168.0.2', //數(shù)據(jù)庫(kù)域名
// ... ...
),
),
//... ...
若db_A服務(wù)器中的數(shù)據(jù)庫(kù)有表a_table_user、a_table_friends,而db_B服務(wù)器中的數(shù)據(jù)庫(kù)有表b_table_article、b_table_comments,則:
<?php
return array(
//... ...
/**
* 自定義路由表
*/
'tables' => array(
//通用路由
'__default__' => array(
'prefix' => 'a_', //以 a_ 為表前綴
'key' => 'id',
'map' => array(
array('db' => 'db_A'), //默認(rèn),使用db_A數(shù)據(jù)庫(kù)
),
),
'table_article' => array( //表b_table_article
'prefix' => 'b_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_B'), // b_table_article表使用db_B數(shù)據(jù)庫(kù)
),
),
'table_comments' => array( //表b_table_article
'prefix' => 'b_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_B'), // b_table_comments表使用db_B數(shù)據(jù)庫(kù)
),
),
),
如果項(xiàng)目存在分表的情況,可結(jié)合上述的分表的說(shuō)明進(jìn)行配置。
這里為了讓大家更為明了,假設(shè)db_A服務(wù)器中的數(shù)據(jù)庫(kù)有表a_table_user、a_table_friends_0到a_table_friends_9(共10張表), 而db_B服務(wù)器中的數(shù)據(jù)庫(kù)有表b_table_article、b_table_comments_0到b_table_comments_19(共20張表),則結(jié)合起來(lái)的完整配置為:
<?php
return array(
/**
* DB數(shù)據(jù)庫(kù)服務(wù)器集群
*/
'servers' => array(
'db_A' => array( //db_A
'host' => '192.168.0.1', //數(shù)據(jù)庫(kù)域名
// ... ...
),
'db_B' => array( //db_B
'host' => '192.168.0.2', //數(shù)據(jù)庫(kù)域名
// ... ...
),
),
/**
* 自定義路由表
*/
'tables' => array(
//通用路由
'__default__' => array(
'prefix' => 'a_', //以 a_ 為表前綴
'key' => 'id',
'map' => array(
array('db' => 'db_A'), //默認(rèn),使用db_A數(shù)據(jù)庫(kù)
),
),
'table_friends' => array( //分表配置
'prefix' => 'a_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_A'), // b_table_comments表使用db_B數(shù)據(jù)庫(kù)
array('start' => 0, 'end' => 9, 'db' => 'db_A'), //分表配置(共10張表)
),
),
'table_article' => array( //表b_table_article
'prefix' => 'b_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_B'), // b_table_article表使用db_B數(shù)據(jù)庫(kù)
),
),
'table_comments' => array( //表b_table_article
'prefix' => 'b_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_B'), // b_table_comments表使用db_B數(shù)據(jù)庫(kù)
array('start' => 0, 'end' => 19, 'db' => 'db_B'), //分表配置(共20張表)
),
),
),
);
雖然這是專門為海量數(shù)據(jù)設(shè)計(jì)的存儲(chǔ)方案,但也是可以結(jié)合主從配置來(lái)獲得更龐大強(qiáng)壯的方案,當(dāng)然為之付出的是復(fù)雜性的引入。
簡(jiǎn)單地,可以將dbs.php復(fù)制一份dbs_slave.php出來(lái)給從庫(kù)使用,然后注冊(cè)一個(gè)從庫(kù)的服務(wù):
DI()->slaveNotorm = new PhalApi_DB_NotORM(DI()->config->get('slave_dbs'));
最后,在需要使用從庫(kù)來(lái)讀取時(shí),使用slaveNotorm 服務(wù)即可。
這樣的設(shè)計(jì)是有明顯的靈活性的,因?yàn)樵诤笃谌绻枰w移數(shù)據(jù)庫(kù)服務(wù)器,我們可以在框架支持的情況下輕松應(yīng)對(duì),但依然需要考慮到一些問(wèn)題和不足。
DB變更,這塊是必不可少的,但一旦數(shù)據(jù)庫(kù)表被拆分后,表數(shù)量的驟增導(dǎo)致變更執(zhí)行困難,所以這里暫時(shí)使用了一個(gè)折中的方案,即提供了一個(gè)ext_data 擴(kuò)展字段用于存放后期可能需要的字段信息,建議采用json格式,因?yàn)橥ㄓ们议L(zhǎng)度比序列化的短。但各開(kāi)發(fā)可以根據(jù)自己的需要決定格式。即使如此,擴(kuò)展字段 明顯做不到一些SQL的查詢及其他操作。
表之間的關(guān)聯(lián)查詢,這個(gè)是分拆后的最大問(wèn)題。雖然這樣的代價(jià)是我們可以得到更龐大的存儲(chǔ)設(shè)計(jì), 而且很多表之間不需要必須的關(guān)聯(lián)的查詢,即使我們需要,我們也可以通過(guò)其他手段如緩存和分開(kāi)查詢來(lái)實(shí)現(xiàn)。這對(duì)開(kāi)發(fā)人員有一定的約束,但是對(duì)于可預(yù)見(jiàn)性的海 量數(shù)量,這又是必須的。
更多建議: