讀discuzx3.1 資料庫層筆記

頹廢的老貓發表於2013-12-10

最近開始在看discuzx3.1的程式碼,看到資料庫層的實現,discuzx的資料庫層能夠支撐資料庫分庫,分散式部署,主要水平分表,也可以很方便的支援其他資料庫。效能上,可以做讀寫分離,支援資料快取。可以說,是一個很完善的資料庫層的解決方案了。

資料庫層分為三層,業務邏輯層封裝,抽象層,和驅動層。如圖:


其中,資料抽象層封裝定義資料庫操作,負責解析sql語句,連線底層驅動執行sql,並資料安全過濾。

資料庫抽象層由discuzx_database類實現,該類所有的成員變數和方法都是靜態的,可以直接呼叫。本類中,init 方法用來初始化底層驅動,設定資料庫配置,並且建立預設的資料庫連線。table方法,設定當前操作的資料表,這個方法很關鍵,資料庫的分散式部署,讀寫分離,都是由表名來確定的。這個在驅動層具體實現。其他如insert,delete,update,fetch_all等等方法是封裝了資料表的基本操作,checkquery方法負責資料的安全檢查,防注入。query方法負責解析sql語句,呼叫驅動層,執行sql語句,quote,field_quote,field,format等方法負責sql語句的格式化。

資料庫驅動層選擇資料庫,直接連線資料庫,跟資料庫互動。目前discuzx預設提供兩種底層驅動,mysql和mysqli。以mysql為例,整個驅動層其實由db_dirver_mysql和db_driver_mysql_slave兩個類外加資料庫配置檔案實現,db_driver_mysql_salve繼承於db_driver_mysql。

配置檔案中(./config/config_global.php )可配置資料表的讀寫分離,包括多臺從伺服器,分散式部署。

 $_config['db']['1']['slave']['1']['dbhost'] = 'localhost';
 $_config['db']['1']['slave']['1']['dbuser'] = 'root';
 $_config['db']['1']['slave']['1']['dbpw'] = 'root';
 $_config['db']['1']['slave']['1']['dbcharset'] = 'gbk';

可配置 禁用從資料庫的資料表, 表名字之間使用逗號分割

 * @example common_session, common_member 這兩個表僅從主伺服器讀寫, 不使用從伺服器
 * $_config['db']['common']['slave_except_table'] = 'common_session, common_member';

可根據資料表進行分散式部署

 @example 將 common_member 部署到第二伺服器, common_session 部署在第三伺服器, 則設定為
 $_config['db']['map']['common_member'] = 2;
 $_config['db']['map']['common_session'] = 3;

先看在抽象層(DB 類)初始化。在discuz_application的init時,呼叫_init_db()方法

private function _init_db() {
if($this->init_db) {
$driver = function_exists('mysql_connect') ? 'db_driver_mysql' : 'db_driver_mysqli';
if(getglobal('config/db/slave')) {
$driver = function_exists('mysql_connect') ? 'db_driver_mysql_slave' : 'db_driver_mysqli_slave';
}
DB::init($driver, $this->config['db']);
}
}

抽象層DB的init

public static function init($driver, $config) {
self::$driver = $driver;
self::$db = new $driver;
self::$db->set_config($config);
self::$db->connect();
}


根據配置檔案中的讀寫分離來確定底層dirver,如果支援從庫,則初始化db_dirver_mysql_salve。

db_driver_mysql類中的table_name方法是實現分庫的鑰匙,根據表名來確定當前需要連線的資料庫伺服器編號,並連線資料庫,存入連線池中,同時設定為當前連線。

function table_name($tablename) {
if(!empty($this->map) && !empty($this->map[$tablename])) {
$id = $this->map[$tablename];
if(!$this->link[$id]) {
$this->connect($id);
}
$this->curlink = $this->link[$id];
} else {
$this->curlink = $this->link[1];
}
return $this->tablepre.$tablename;
}

這裡提一下,$this->link可以實現資料庫只需要連線一次,不需要重複連線。

在寫入資料時,呼叫主庫,而讀取資料時呼叫從庫,見db_driver_mysql_salve, 該類覆蓋了table_name表,判斷該表是否需要讀寫分離,並且確定該表的資料庫伺服器組編號

public function table_name($tablename) {
$this->tablename = $tablename;
if(!$this->slaveexcept && $this->excepttables) {
$this->slaveexcept = in_array($tablename, $this->excepttables, true);
}
$this->serverid = isset($this->map[$this->tablename]) ? $this->map[$this->tablename] : 1;
return $this->tablepre.$tablename;
}

重寫了query方法,判斷如果當前需要讀從庫則選擇並連線從庫。

public function query($sql, $silent = false, $unbuffered = false) {
if(!(!$this->slaveexcept && strtoupper(substr($sql, 0 , 6)) === 'SELECT' && $this->_slave_connect())) {
$this->_master_connect();
}
$this->tablename = '';
$this->slaveexcept = false;
return parent::query($sql, $silent, $unbuffered);
}

db_dirver_mysql中其他方法的用途。set_config方法,用於載入配置資訊。content方法,根據伺服器號連線相應資料庫,存入連線池,並設定為當前連線。query使用當前連線執行sql語句。fetch_all,fetch_first,result_first定義常用操作。

db_dirver_mysql_slave中的其他方法的用途。set_config執行父類方法,設定不需要讀寫分離的資料表。_choose_slave方法,在多臺從伺服器的情況下,根據權重選擇一臺從庫。

至此,資料庫的抽象層和底層基本清楚了。

業務邏輯層其實是對抽象層的封裝。邏輯層其實是所有的業務邏輯中涉及資料庫操作的部分。都能直接通過抽象層的DB::query或者DB::insert等方法來讀寫資料。不過通常資料表的操作都再次進行了封裝。資料表業務類在source/class/table目錄中。資料表操作類都繼承於discuz_table類,該在在source/calss/discuz目錄下。資料庫的資料快取也在本層中實現。

__coustruct方法,設定表名和主鍵,設定是否快取和快取時間。該類主要封裝了一些常用資料庫操作,count,insert,delete,fetch,fetch_all等。資料快取相關的方法:獲取指定鍵值的資料快取fetch_cache,儲存單條記錄快取store_cache,清除指定記錄快取clear_cache,update_cache 更新指定單條記錄快取,update_batch_cache 批量更新指定記錄快取,reset_cache 重置指定資料的快取,increase_cache 在原有快取上追加更新快取。快取操作的方法由實際資料庫操作的方法呼叫,如fetch方法:

public function fetch($id, $force_from_db = false){
$data = array();
if(!empty($id)) {
if($force_from_db || ($data = $this->fetch_cache($id)) === false) {
$data = DB::fetch_first('SELECT * FROM '.DB::table($this->_table).' WHERE '.DB::field($this->_pk, $id));
if(!empty($data)) $this->store_cache($id, $data);
}
}
return $data;
}

public function store_cache($id, $data, $cache_ttl = null, $pre_cache_key = null) {
$ret = false;
if($this->_allowmem) {
if($pre_cache_key === null)$pre_cache_key = $this->_pre_cache_key;
if($cache_ttl === null)$cache_ttl = $this->_cache_ttl;
$ret = memory('set', $id, $data, $cache_ttl, $pre_cache_key);
}
return $ret;
}

判斷是否快取和快取中是否存,如果是則直接返回快取,而不讀資料庫。從資料庫中讀出資料後,存入快取中。而是否從快取中讀取時由外部引數來定義的,預設是可快取。本方法中的快取讀寫用通過memory來實現的,memory方法定義在function_core檔案中,至於快取的具體實現,需要在另外一篇文章中說明了。


至此,discuzx3.1的資料庫層都有了,分析的有點凌亂。


相關文章