Yii實現MySQL多庫和讀寫分離

Mark一下,以後遲早得用,以備不時之需

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

前段時間爲SNS產品做了架構設計,在程序框架方面做了不少相關的壓力測試,最終選定了YiiFramework,至於爲什麼沒選用公司內部的PHP框架,其實理由很充分,公司的框架雖然是"前輩"們辛苦的積累,但畢竟不夠成熟,沒有大型項目的歷練,猶如一個涉世未深的年輕小夥。Yii作爲一個頗有名氣開源產品,必定有很多人在使用,意味着有一批人在維護,而且在這之前,我也使用Yii開發過大型項目,Yii的設計模式和它的易擴展特性足以堪當重任。

SNS同一般的社交產品不同的就是它最終要承受大併發和大數據量的考驗,架構設計時就要考慮這些問題, web分佈式、負載均衡、分佈式文件存儲、MySQL分佈式或讀寫分離、NoSQL以及各種緩存,這些都是必不可少的應用方案,本文所講的就是MySQL分庫和主從讀寫分離在Yii的配置和使用。

Yii默認是不支持讀寫分離的,我們可以利用Yii的事件驅動模式來實現MySQL的讀寫分離。

Yii提供了一個強大的CActiveRecord數據庫操作類,通過重寫getDbConnection方法來實現數據庫的切換,然後通過事件 beforeSave、beforeDelete、beforeFind 來實現讀寫服務器的切換,還需要兩個配置文件dbconfig和modelconfig分別配置數據庫主從服務器和model所對應的數據庫名稱

以下是實現代碼

這是我寫好的類,放在components文件夾裏,然後所有的Model都繼承ActiveRecord類就可以實現多庫和主從讀寫分離了

DBConfig.php

<?php

return array(
    'passport' => array(
        'write' => array(
            'class' => 'CDbConnection',
            'connectionString' => 'mysql:host=10.1.39.2;dbname=db1',
            'emulatePrepare' => true,
            //'enableParamLogging' => true,
            'enableProfiling' => true,
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8',
            'schemaCachingDuration' => 3600,
        ),
        'read' => array(
            array(
                'class' => 'CDbConnection',
                'connectionString' => 'mysql:host=10.1.39.3;dbname=db1',
                'emulatePrepare' => true,
                //'enableParamLogging' => true,
                'enableProfiling' => true,
                'username' => 'root',
                'password' => '',
                'charset' => 'utf8',
                'schemaCachingDuration' => 3600,
            ),
            array(
                'class' => 'CDbConnection',
                'connectionString' => 'mysql:host=10.1.39.4;dbname=db3',
                'emulatePrepare' => true,
                //'enableParamLogging' => true,
                'enableProfiling' => true,
                'username' => 'root',
                'password' => '',
                'charset' => 'utf8',
                'schemaCachingDuration' => 3600,
            ),
        ),
    ),
);

ModelConfig.php

<?php

return array(
    //key爲數據庫名稱,value爲Model
    'passport' => array('User', 'Post'),
    'microblog' => array('…'),
);

?>

ActiveRecord.php

<?php

/**
 * 基於CActiveRecord類的封裝,實現多庫和主從讀寫分離
 * 所有Model都必須繼承些類.
 *
 * @author atao<[email protected]>
 */
class ActiveRecord extends CActiveRecord {
    //model配置
    public $modelConfig = '';

    //數據庫配置
    public $dbConfig = '';

    //定義一個多數據庫集合
    static $dataBase = array();

    //當前數據庫名稱
    public $dbName = '';

    //定義庫類型(讀或寫)
    public $dbType = 'read'; //'read' or 'write'

    /**
     * 在原有基礎上添加了一個dbname參數
     * @param string $scenario Model的應用場景
     * @param string $dbname 數據庫名稱
     */
    public function __construct($scenario = 'insert', $dbname = '') {
        if (!empty($dbname))
            $this->dbName = $dbname;

        parent::__construct($scenario);
    }

    /**
     * 重寫父類的getDbConnection方法
     * 多庫和主從都在這裏切換
     */
    public function getDbConnection() {
        //如果指定的數據庫對象存在則直接返回
        if (self::$dataBase[$this->dbName] !== null)
            return self::$dataBase[$this->dbName];

        if ($this->dbName == 'db') {
            self::$dataBase[$this->dbName] = Yii::app()->getDb();
        } else {
            $this->changeConn($this->dbType);
        }

        if (self::$dataBase[$this->dbName] instanceof CDbConnection) {
            self::$dataBase[$this->dbName]->setActive(true);
            return self::$dataBase[$this->dbName];
        } else
            throw new CDbException(Yii::t('yii', 'Model requires a "db" CDbConnection application component.'));

    }

    /**
     * 獲取配置文件
     * @param unknown_type $type
     * @param unknown_type $key
     */
    private function getConfig($type = "modelConfig", $key = '') {
        $config = Yii::app()->params[$type];
        if ($key)
            $config = $config[$key];
        return $config;
    }

    /**
     * 獲取數據庫名稱
     */
    private function getDbName() {
        if ($this->dbName)
            return $this->dbName;
        $modelName = get_class($this->model());
        $this->modelConfig = $this->getConfig('modelConfig');
        //獲取model所對應的數據庫名
        if ($this->modelConfig) foreach ($this->modelConfig as $key => $val) {
            if (in_array($modelName, $val)) {
                $dbName = $key;
                break;
            }
        }
        return $dbName;
    }

    /**
     * 切換數據庫連接
     * @param unknown_type $dbtype
     */
    protected function changeConn($dbtype = 'read') {

        if ($this->dbType == $dbtype && self::$dataBase[$this->dbName] !== null)
            return self::$dataBase[$this->dbName];

        $this->dbName = $this->getDbName();

        if (Yii::app()->getComponent($this->dbName . '_' . $dbtype) !== null) {
            self::$dataBase[$this->dbName] = Yii::app()->getComponent($this->dbName . '_' . $dbtype);
            return self::$dataBase[$this->dbName];
        }

        $this->dbConfig = $this->getConfig('dbConfig', $this->dbName);

        //跟據類型取對應的配置(從庫是隨機值)
        if ($dbtype == 'write') {
            $config = $this->dbConfig[$dbtype];
        } else {
            $slavekey = array_rand($this->dbConfig[$dbtype]);
            $config = $this->dbConfig[$dbtype][$slavekey];
        }

        //將數據庫配置加到component中
        if ($dbComponent = Yii::createComponent($config)) {
            Yii::app()->setComponent($this->dbName . '_' . $dbtype, $dbComponent);
            self::$dataBase[$this->dbName] = Yii::app()->getComponent($this->dbName . '_' . $dbtype);
            $this->dbType = $dbtype;
            return self::$dataBase[$this->dbName];
        } else
            throw new CDbException(Yii::t('yii', 'Model requires a "changeConn" CDbConnection application component.'));
    }

    /**
     * 保存數據前選擇 主 數據庫
     */
    protected function beforeSave() {
        parent::beforeSave();
        $this->changeConn('write');
        return true;
    }

    /**
     * 刪除數據前選擇 主 數據庫
     */
    protected function beforeDelete() {
        parent::beforeDelete();
        $this->changeConn('write');
        return true;
    }

    /**
     * 讀取數據選擇 從 數據庫
     */
    protected function beforeFind() {
        parent::beforeFind();
        $this->changeConn('read');
        return true;
    }

    /**
     * 獲取master庫對象
     */
    public function dbWrite() {
        return $this->changeConn('write');
    }

    /**
     * 獲取slave庫對象
     */
    public function dbRead() {
        return $this->changeConn('read');
    }
}

?>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章