海量數(shù)據(jù):可配置的分庫(kù)分表

2018-11-21 21:19 更新

2.12.1 背景

為了應(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ù)器上的限制。

2.12.2 主要思想

  • 1、分庫(kù)分表
  • 2、路由規(guī)則
  • 3、擴(kuò)展字段
  • 4、可配置
  • 5、SQL語(yǔ)句自動(dòng)生成

(1)分庫(kù)分表

是指將不需要進(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份。

(2)路由規(guī)則

在上面進(jìn)行了分庫(kù)分表后,開(kāi)發(fā)人員在讀取時(shí),就需要根據(jù)相應(yīng)的規(guī)則找到對(duì)應(yīng) 的數(shù)據(jù)庫(kù)和數(shù)據(jù)庫(kù)表,這里建議每個(gè)表都需要有int(11)類型的id字段,以便作為分表的參考。

(3)擴(kuò)展字段

在完成了分庫(kù)分表和制定路由規(guī)則后,考慮到日后有數(shù)據(jù)庫(kù)的DB變更,為減少DB變更對(duì)現(xiàn)有數(shù)據(jù)庫(kù)表的影響,這里建議每個(gè)表都增加text類型的extra_data字段,并且使用json格式進(jìn)行轉(zhuǎn)換存儲(chǔ)。

(4)可配置

在有了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)部 分。

(5)SQL語(yǔ)句自動(dòng)生成

對(duì)于相同表的建表語(yǔ)句,可以通過(guò)腳本來(lái)自動(dòng)生成,然后直接導(dǎo)入數(shù)據(jù)即可。

2.12.3 PhalApi框架的實(shí)現(xiàn)方案

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ī)則。 show如上圖所示,表名會(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)支持。

2.12.4 使用示例

(1)配置數(shù)據(jù)庫(kù)的路由配置

修改./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

(2)準(zhǔn)備需要?jiǎng)?chuàng)建表的基本SQL語(yǔ)句

這里說(shuō)的基本SQL語(yǔ)句是指:僅是這個(gè)表所特有的字段,排除已固定公共有的自增主鍵id,和擴(kuò)展字段ext_data。下面是一個(gè)示例:

`name` varchar(11) DEFAULT NULL,

(3)生成并導(dǎo)入SQL語(yǔ)句

由于拆分后的數(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;

(4)使用與代碼開(kāi)發(fā)

在將上面的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();

使用Model基類的情況

更好的寫法,應(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'] : '';
    }

}

2.12.5 多個(gè)數(shù)據(jù)庫(kù)的配置方式

當(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張表)
            ),
        ),
    ),
);

2.12.6 與主從數(shù)據(jù)庫(kù)的有機(jī)結(jié)合

雖然這是專門為海量數(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ù)即可。

2.12.7 不足與注意點(diǎn)

這樣的設(shè)計(jì)是有明顯的靈活性的,因?yàn)樵诤笃谌绻枰w移數(shù)據(jù)庫(kù)服務(wù)器,我們可以在框架支持的情況下輕松應(yīng)對(duì),但依然需要考慮到一些問(wèn)題和不足。

(1)DB變更

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的查詢及其他操作。

(2)表之間的關(guān)聯(lián)查詢

表之間的關(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ù)量,這又是必須的。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)