|
前文我们已经介绍过 Discuz!X 实现数据库分布式部署、读写分离以及负载均衡的具体配置方法,此文将从源码的角度继续分析这些功能的实现原理。
首先需要说明的是,分布式部署、读写分离和负载均衡本质上说的都是一回事,即他们解决的问题都是将用户的访问流量分摊到不同的设备(节点)来进行处理,以最大化整个系统的并发处理性能。只不过侧重稍有区别,比如分布式部署的侧重点是分表分库,将个别并发访问比较大的数据表单独建库并在独立的数据库服务器下运行,适合读写比例较均匀的业务模型;而读写分离则适合读操作远远多余写操作的业务模型,各种以内容展示为核心业务的应用(论坛、CMS、博客等等)都属于这种,且读写分离后,可以实现 1 拖 N 的架构,实现读流量的分流处理也就实现了负载均衡。
Discuz!X 采用了面向对象设计方法,并将所有数据库的操作封装在相关的类中,主要分为 驱动类 和 操作类 两种。
驱动类源码位于目录:./source/class/db
其中有四个文件:
1、db_driver_mysql.php;
2、db_driver_mysqli.php;
3、db_driver_mysql_slave.php;
4、db_driver_mysqli_slave;
其中 db_driver_mysql.php 和 db_driver_mysql_slave.php 是用于兼容老版本的 PHP,结构和成员函数也与 mysqli 的版本基本一致,因此我们主要分析 db_driver_mysqli.php 和 db_driver_mysqli_slave.php 这两个文件。
当 Discuz!X 内核加载的时候,会自动加载数据库驱动,参考以下代码片段:
- 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']);
- }
- }
复制代码 此段代码位于:./source/class/discuz/discuz_application.php,用于初始化数据库驱动,首先代码通过检测是否存在函数 mysql_connect 来判断应该加载的驱动类的名称,如果 Discuz 配置文件中开启了 slave 功能,则加载带有 slave 后缀的类。
代码中的 DB 仅仅是 discuz_database 类的别名,参考代码(./source/class/class_core.php):
- class DB extends discuz_database {}
复制代码
操作类源码还可以细分为两类,其中 discuz_database (./source/class/discuz/discuz_database.php)用于封装各类 SQL 查询(用静态成员函数方式实现),属于基础的数据库操作类;
另外还有各种 Table 类封装了与数据表有关的各种操作,这些类与应用的业务模型结合更加紧密。Discuz!X 中用到的数据表均有一个对应的 Table 类,存放在目录:./source/class/table 下,它们都由 discuz_table 类 extends 而来。discuz_table 类文件位于: ./source/class/discuz/discuz_table.php。
那么这些类之间的调用关系是怎样的呢?首先在内核加载完成后,Discuz!X 源码中所有对数据表的操作均通过 C::t() 静态成员函数执行,其中 C 是 Discuz!X 内核对象的别名(与 DB 类似),t 则是内核对象的成员函数用于实例化对应的数据表类,例如:
- C::t('common_member')->fetch(1);
复制代码 将返回 uid 为 1 的用户数据。查阅 table_common_member 类源码(./source/class/table/table_common_member.php)得知,table_common_member 类并没有定义成员函数 fetch(),即上述代码使用了父类的成员函数,参考代码如下(./source/class/discuz/discuz_table.php):
- 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;
- }
复制代码 可以看到,最终的查询操作是通过代码:
- $data = DB::fetch_first('SELECT * FROM '.DB::table($this->_table).' WHERE '.DB::field($this->_pk, $id));
复制代码 实现。再继续看看 DB 的静态成员函数 ::table 和 ::fetch_first,参考代码:
- public static function table($table) {
- return self::$db->table_name($table);
- }
复制代码
代码中 self::$db 则是在 Discuz!X 内核实例化过程中加载过的 db_driver_mysqli 类 或者 db_driver_mysqli_slave 类,然后由 self::$db->query 完成对数据库的最终访问。
说了这么多,实现分布式以及读写分离的代码到底在哪里呢?别急,让我们继续分解 db_driver_mysqli 类和 db_driver_mysqli_slave 类。首先来看看成员函数 table_name,此成员函数在 db_driver_mysqli 和 db_driver_mysqli_slave 中的定义不太一样,即 db_driver_mysqli_slave 重载了该成员函数,现在让我们分别来看看他们是如何定义的,先看 db_driver_mysqli 中的 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;
- }
复制代码 代码实现的功能如下,首先检测配置中是否配置了 map 映射,如果有则将当前数据库连接指向 map 配置所指向的数据库,没有则使用默认数据库。
而 slave 类中的 table_name 除了类似的操作,还对 slaveexcept 进行了处理,参考代码如下:
- 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;
- }
复制代码 所以,实现数据库的分布式部署,就是通过 table_name 成员函数配合对应的配置文件实现。
同时,db_driver_mysqli_slave 类是 db_driver_mysqli_slave 类的父类,相对于父类,db_driver_mysqli_slave 提供了这么几个成员函数:
1、protected function _slave_connect;
2、protected function _choose_slave;
3、protected function _master_connect;
当具体执行某个 query 的时候,会首先判断要访问的数据表是否在 slave except 指定的范围内,如果不在则继续判断是否为 SELECT 查询,只有 SELECT 查询才会路由到 Slave 服务器执行访问。并且在路由之前,由 _slave_connect 调用 _choose_slave 随机选择一个 Slave 服务器,如果配置了 Slave 的 weight,则会根据 weight 值加权进行选择合适的 Slave。
除此之外的其他的查询则通过 _master_connect 路由到主服务器。
总结,Discuz!X 的数据库访问操作大致可以划分为三个层次,最上层为 Table 类对应到每一张数据表,相关操作与 Discuz!X 业务模型紧密相连;中间层为 DB 类,封装了绝大部分 SQL 查询;底层是驱动层,负责执行最终的 SQL 查询,并支持通过数据表的map因素实现分布式部署,以及 slave 的相关配置实现读写分离,和读操作的负载均衡。
|
|