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