##2.12.1 背景
為了應對產品海量用戶的愿景需求,這里將設計一個分布式的數據庫存儲方案,以便能滿足數據量的驟增、云服務的橫向擴展、后臺接口開發的兼容性,以及數據遷移等問題,避免日后因為全部數據都存放在單臺服務器上的限制。
##2.12.2 主要思想
+ 1、分庫分表
+ 2、路由規則
+ 3、擴展字段
+ 4、可配置
+ 5、SQL語句自動生成
###(1)分庫分表
是指將不需要進行必要關聯查詢的表分開存放,如存放事件推送的weili_event_pushto和存放標簽的weili_tag;同時,對于 同一個表,因為存放的數據量是可預見式的暴增,如上述的weili_event_pushto,每時每刻都會產生大量的來自用戶發布的事件,因此為了突破 MySQL單表的限制以及其他問題,需要將此表同時創建N份。
###(2)路由規則
在上面進行了分庫分表后,開發人員在讀取時,就需要根據相應的規則找到對應 的數據庫和數據庫表,這里建議每個表都需要有int(11)類型的id字段,以便作為分表的參考。
###(3)擴展字段
在完成了分庫分表和制定路由規則后,考慮到日后有數據庫的DB變更,為減少DB變更對現有數據庫表的影響,這里建議每個表都增加text類型的extra_data字段,并且使用json格式進行轉換存儲。
###(4)可配置
在有了N臺數據庫服務器以及每個表都拆分成M張表后,為減少后臺接口開發人員的壓力,有必須在后臺接口框架提供可配置 的支持。即:數據庫的變更不應影響開發人員現有的開發,也不需要開發人員作出代碼層面的改動,只需要稍微配置一下即可。關于這塊,請見下面的框架實現部 分。
###(5)SQL語句自動生成
對于相同表的建表語句,可以通過腳本來自動生成,然后直接導入數據即可。
##2.12.3 PhalApi框架的實現方案
PhalApi框架主要需要實現的是路由這一層的映射,并且通過可配置的方式進行控制,同時還應支持生產環境和測試環境的異同,如在測試環境我們明顯不需要1000張數據庫的表。為此,需要提供一種 **表名 + id** 映射到 **數據庫服務器 + 具體哪張表** 的規則。

如上圖所示,表名會統一加上前綴,并且將id按一定的表總數進行取模,最后再根據得到的具體表名,通過映射表查找到對應 的數據庫服務器進行操作。其中,model層為開發實現,數據庫表的映射由接口框架實現支持。
##2.12.4 使用示例
###(1)配置數據庫的路由配置
修改./Config/dbs.php文件,以下是參考的示例配置。其中servers為DB服務器,包括數據庫的賬號信息等,tables為數據庫表的映射關系,其中__default__下標為缺省的數據庫路由。
在每個數據庫表里面,可以配置多個數據庫表,通過開始的下標start和結束的下標end來對表進行分布式存放,并且如果沒有start和end的,則視為不需要拆分存放,同時也是當找不到合適時的拆分表時所采用的默認配置。
```javascript
return array(
/**
* avaiable db servers
*/
'servers' => array(
'db_demo' => array(
'host' => 'localhost', //數據庫域名
'name' => 'test', //數據庫名字
'user' => 'root', //數據庫用戶名
'password' => '123456', //數據庫密碼
'port' => '3306', //數據庫端口
),
),
/**
* 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'),
),
),
),
);
```
上面示例配置的意思是:
```javascript
表名 DB服務器
tbl_demo db_demo
tbl_demo_0 db_demo
tbl_demo_1 db_demo
tbl_demo_2 db_demo
```
###(2)準備需要創建表的基本SQL語句
這里說的基本SQL語句是指:僅是這個表所特有的字段,排除已固定公共有的自增主鍵id,和擴展字段ext_data。下面是一個示例:
```javascript
`name` varchar(11) DEFAULT NULL,
```
###(3)生成并導入SQL語句
由于拆分后的數據庫表數量眾多,這里提供了一個快捷的腳本工具來生成所需要創建的數據庫表。
```javascript
$ php ./build_sqls.php
Usage: ./build_sqls.php <table> [engine=InnoDB]
```
執行上面的腳本,輸入數據庫表參數后:
```javascript
php ./build_sqls.php demo
```
將會從配置文件 里面尋找所需要創建的表,并生成類似以下的SQL語句:
```javascript
/**
* 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;
```
###(4)使用與代碼開發
在將上面的SQL語句導入數據庫后,即可以像之前那樣操作數據庫。下面是一些示例:
```javascript
DI()->notorm = new PhalApi_DB_NotORM(PhalApi_DI::one()->config->get('dbs'), true);
DI()->notorm->demo->where('id', '1')->fetch();
```
用到了拆分表的代碼示例,假設event表被拆分成了3個表,則客戶端在調用里,需要根據(id % 3 )來拼接合適的數據庫表名,其他使用不變。
```javascript
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();
```
####使用Model基類的情況
更好的寫法,應該是繼承于PhalApi_Model_NotORM,并統一實現分表的操作,如:
```javascript
<?php
class Model_Demo extends PhalApi_Model_NotORM {
protected function getTableName($id) {
$tableName = 'demo';
if ($id !== null) {
$tableName .= '_' . ($id % 3);
}
return $tableName;
}
}
```
然后,上面的查詢分別對應:
```javascript
$model = new Model_Demo();
$row = $model->get('3', 'id');
$row = $model->get('10', 'id');
$row = $model->get('2', 'id');
```
更進一步,我們可以通過$this->getORM($id)來獲取分表的實例進行分表的操作,如:
```javascript
<?php
class Model_Demo extends PhalApi_Model_NotORM {
//... ...
public function getNameById($id) {
$row = $this->getORM($id)->select('name')->fetchRow(); //假設$id為3,則 $this->getORM($id) 等效于 DI()->notorm->demo_0
return !empty($row) ? $row['name'] : '';
}
}
```
##2.12.5 多個數據庫的配置方式
當需要使用多個數據庫時,可以先在servers中可以配置多組數據庫的信息,然后在tables為不同的數據庫表指定不同的數據庫服務器。
假設我們有兩臺數據庫服務器,分別叫做db_A、db_B,即:
```javascript
return array(
/**
* DB數據庫服務器集群
*/
'servers' => array(
'db_A' => array( //db_A
'host' => '192.168.0.1', //數據庫域名
// ... ...
),
'db_B' => array( //db_B
'host' => '192.168.0.2', //數據庫域名
// ... ...
),
),
//... ...
```
若db_A服務器中的數據庫有表a_table_user、a_table_friends,而db_B服務器中的數據庫有表b_table_article、b_table_comments,則:
```javascript
<?php
return array(
//... ...
/**
* 自定義路由表
*/
'tables' => array(
//通用路由
'__default__' => array(
'prefix' => 'a_', //以 a_ 為表前綴
'key' => 'id',
'map' => array(
array('db' => 'db_A'), //默認,使用db_A數據庫
),
),
'table_article' => array( //表b_table_article
'prefix' => 'b_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_B'), // b_table_article表使用db_B數據庫
),
),
'table_comments' => array( //表b_table_article
'prefix' => 'b_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_B'), // b_table_comments表使用db_B數據庫
),
),
),
```
如果項目存在分表的情況,可結合上述的分表的說明進行配置。
這里為了讓大家更為明了,假設db_A服務器中的數據庫有表a_table_user、a_table_friends_0到a_table_friends_9(共10張表),
而db_B服務器中的數據庫有表b_table_article、b_table_comments_0到b_table_comments_19(共20張表),則結合起來的完整配置為:
```javascript
<?php
return array(
/**
* DB數據庫服務器集群
*/
'servers' => array(
'db_A' => array( //db_A
'host' => '192.168.0.1', //數據庫域名
// ... ...
),
'db_B' => array( //db_B
'host' => '192.168.0.2', //數據庫域名
// ... ...
),
),
/**
* 自定義路由表
*/
'tables' => array(
//通用路由
'__default__' => array(
'prefix' => 'a_', //以 a_ 為表前綴
'key' => 'id',
'map' => array(
array('db' => 'db_A'), //默認,使用db_A數據庫
),
),
'table_friends' => array( //分表配置
'prefix' => 'a_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_A'), // b_table_comments表使用db_B數據庫
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數據庫
),
),
'table_comments' => array( //表b_table_article
'prefix' => 'b_', //表名前綴
'key' => 'id', //表主鍵名
'map' => array( //表路由配置
array('db' => 'db_B'), // b_table_comments表使用db_B數據庫
array('start' => 0, 'end' => 19, 'db' => 'db_B'), //分表配置(共20張表)
),
),
),
);
```
##2.12.6 與主從數據庫的有機結合
雖然這是專門為海量數據設計的存儲方案,但也是可以結合主從配置來獲得更龐大強壯的方案,當然為之付出的是復雜性的引入。
簡單地,可以將dbs.php復制一份dbs_slave.php出來給從庫使用,然后注冊一個從庫的服務:
```javascript
DI()->slaveNotorm = new PhalApi_DB_NotORM(DI()->config->get('slave_dbs'));
```
最后,在需要使用從庫來讀取時,使用slaveNotorm 服務即可。
##2.12.7 不足與注意點
這樣的設計是有明顯的靈活性的,因為在后期如果需要遷移數據庫服務器,我們可以在框架支持的情況下輕松應對,但依然需要考慮到一些問題和不足。
###(1)DB變更
DB變更,這塊是必不可少的,但一旦數據庫表被拆分后,表數量的驟增導致變更執行困難,所以這里暫時使用了一個折中的方案,即提供了一個ext_data 擴展字段用于存放后期可能需要的字段信息,建議采用json格式,因為通用且長度比序列化的短。但各開發可以根據自己的需要決定格式。即使如此,擴展字段 明顯做不到一些SQL的查詢及其他操作。
###(2)表之間的關聯查詢
表之間的關聯查詢,這個是分拆后的最大問題。雖然這樣的代價是我們可以得到更龐大的存儲設計, 而且很多表之間不需要必須的關聯的查詢,即使我們需要,我們也可以通過其他手段如緩存和分開查詢來實現。這對開發人員有一定的約束,但是對于可預見性的海 量數量,這又是必須的。
- 歡迎使用PhalApi!
- 接口,從簡單開始!
- [1.1]-下載與安裝
- [1.2]-創建一個自己的項目
- [1.3]-在線體驗
- [1.4]-文檔、幫助和官網
- [1.10]-對PhalApi框架的抉擇
- [1.11]-快速入門(backup)
- [1.12]-參數規則:接口參數規則配置
- [1.13]-統一的接口請求方式:_sevice=XXX.XXX
- [1.14]-統一的返回格式和結構:ret-data-msg
- [1.15]-數據庫操作:基于NotORM的使用及優化
- [1.16]-配置讀取:內外網環境配置的完美切換
- [1.17]-日記紀錄:簡化版的日記接口
- [1.18]-快速函數:人性化的關懷
- [1.19]-DI服務速查:各資源服務一覽表
- [1.20]-DB操作:數據庫基本操作速查
- [1.21]-類的自動加載:遵循PEAR包的命名規范
- [1.22]-簽名驗證:自定義簽名規則
- [1.23]-請求和響應:GET和POST兩者皆可得及超越JSON格式返回
- [1.24]-緩存策略:更靈活地可配置化的多級緩存
- [1.25]-國際化翻譯:為走向國際化提前做好翻譯準備
- [1.26]-數據安全:數據對稱加密方案
- [1.27]-精益開發:更富表現力的Model層和重量級數據獲取的應對方案
- [1.28]-COOKIE:對COOKIE原生態的支持及記憶加密升級版
- [1.29]-開放與封閉:多入口和統一初始化
- [1.30]-保持的力量:接口開發最佳實踐
- [1.31]-新型計劃任務:以接口形式實現的計劃任務
- [2.11]-核心思想:DI依賴注入-讓資源更可控
- [2.12]-海量數據:可配置的分庫分表
- [2.13]-接口調試:在線SQL語句查看與性能優化
- [2.14]-測試驅動開發:意圖導向編程下的接口開發
- [2.15]-演進:新型計劃任務續篇
- [2.16]-領域驅動設計:應對復雜領域業務的Domain層
- [2.17]-微服務:Api接口服務層
- [2.18]-定制化:資源服務的再實現
- [2.19]-擴展庫:可重用的擴展類庫
- [2.20]-約定編程:架構明顯的編程風格
- [2.21]-服務器統一部署方案簡明版:CentOs---Nginx---php-fpm---MySql-[--Memcached]
- [2.22]-更多工具:精益項目和團隊建設
- [3.1]-擴展類庫:微信開發
- [3.2]-擴展類庫:代理模式下phprpc協議的輕松支持
- [3.3]-擴展類庫:基于PHPMailer的郵件發送
- [3.4]-擴展類庫:優酷開放平臺接口調用
- [3.5]-擴展類庫:七牛云存儲接口調用
- [3.6]-擴展類庫:新型計劃任務
- [3.8]-擴展類庫:用戶、會話和第三方登錄集成
- [3.9]-擴展類庫:swoole支持下的長鏈接和異步任務實現
- [3.11]-擴展類庫:基于FastRoute的快速路由
- [4.2]-開發實戰2:模擬優酷開放平臺接口項目開發
- [4.3]-開發實戰3:一個簡單的小型項目開發(奔跑吧兄弟投票活動)
- [5.1]-架構與思想:PhalApi核心設計和思想解讀
- [5.2]-雜談:扯一些PhalApi的前世和今生
- [5.3]-框架總結:術語表和PHP開發建議
- [5.4]-許可
- [5.5]-聯系和加入我們
- [5.6]-更新日記
- [5.8]-致框架貢獻者:加入PhalApi開源指南
- [6.1]-基于接口查詢語言的SDK包
- [6.2]-SDK包(JAVA版)
- [6.3]-SDK包(PHP版)
- [6.4]-SDK包(Objective-C版)
- [6.5]-SDK包(javascript版)
- [6.6]-SDK包(Ruby版)
- [8.1]-PhalApi視頻教程
- 附錄1:接口文檔參考模板