最近开始在看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的数据库层都有了,分析的有点凌乱。