ThinkPHP读写分离的内部实现

分析的版本是TP3.2.3

此版本已经支持mysql数据库读写分离,配置比较简单,具体可看分布式数据库支持

我们尝试分析下源码实现:

不妨先yy一下,在写操作和读操作时区分开数据库连接分别连接master和slave。

我们可以看见Model.class.php的db初始化

public function db($linkNum = , $config = , $force = false) {
  if ( === $linkNum && $this->db) {
    return $this->db;
  }
  
  if (! isset ( $this->_db [$linkNum] ) || $force) {
    // 创建一个新的实例
    if (! empty ( $config ) && is_string ( $config ) && false === strpos ( $config, / )) { // 支持读取配置参数
      $config = C ( $config );
    }
    $this->_db [$linkNum] = Db::getInstance ( $config );
  } elseif (NULL === $config) {
    $this->_db [$linkNum]->close (); // 关闭数据库连接
    unset ( $this->_db [$linkNum] );
    return;
  }
  
  // 切换数据库连接
  $this->db = $this->_db [$linkNum];
  $this->_after_db ();
  // 字段检测
  if (! empty ( $this->name ) && $this->autoCheckFields)
    $this->_checkTableInfo ();
  return $this;
}

接着看DB单例,

static public function getInstance($config = array()) {
  $md5 = md5 ( serialize ( $config ) );
  if (! isset ( self::$instance [$md5] )) {
    // 解析连接参数 支持数组和字符串
    $options = self::parseConfig ( $config );
    // 兼容mysqli
    if (mysqli == $options [type])
      $options [type] = mysql;
      // 如果采用lite方式 仅支持原生SQL 包括query和execute方法
    $class = $options [lite] ? ThinkDbLite : Think\Db\Driver\ . ucwords ( strtolower ( $options [type] ) );
    if (class_exists ( $class )) {
      self::$instance [$md5] = new $class ( $options );
    } else {
      // 类没有定义
      E ( L ( _NO_DB_DRIVER_ ) . :  . $class );
    }
  }
  self::$_instance = self::$instance [$md5];
  return self::$_instance;
}

接着看见mysql驱动继承了抽象类Driver。

不妨找两个Model操作读(find)和写(add):

public function find($options = array()) {
  ...
  $resultSet = $this->db->select ( $options );
  ...
}


public function add($data = , $options = array(), $replace = false) {
  ...
  $result = $this->db->insert ( $data, $options, $replace );
  ...
}

我们只聚焦关键代码select 和insert 。
然后去查看数据库驱动类代码:

public function select($options = array()) {
  ...
  $result = $this->query ( $sql, ! empty ( $options [fetch_sql] ) ? true : false );
  ...
}

public function insert($data, $options = array(), $replace = false) {
  ...
  return $this->execute ( $sql, ! empty ( $options [fetch_sql] ) ? true : false );
}

继续寻根,我们看见

/**
 * 执行查询 返回数据集
 */
public function query($str, $fetchSql = false) {
  $this->initConnect ( false );
  ...
}

/**
 * 执行语句
 */
public function execute($str, $fetchSql = false) {
  $this->initConnect ( true );
  ...
}

我们终于找到了根本,

/**
	 * 初始化数据库连接
	 * 
	 * @access protected
	 * @param boolean $master
	 *        	主服务器
	 * @return void
	 */
	protected function initConnect($master = true) {
		if (! empty ( $this->config [deploy] ))
			// 采用分布式数据库
			$this->_linkID = $this->multiConnect ( $master );
		else 
		// 默认单数据库
		if (! $this->_linkID)
			$this->_linkID = $this->connect ();
	}
	
	/**
	 * 连接分布式服务器
	 * 
	 * @access protected
	 * @param boolean $master
	 *        	主服务器
	 * @return void
	 */
	protected function multiConnect($master = false) {
		// 分布式数据库配置解析
		$_config [username] = explode ( ,, $this->config [username] );
		$_config [password] = explode ( ,, $this->config [password] );
		$_config [hostname] = explode ( ,, $this->config [hostname] );
		$_config [hostport] = explode ( ,, $this->config [hostport] );
		$_config [database] = explode ( ,, $this->config [database] );
		$_config [dsn] = explode ( ,, $this->config [dsn] );
		$_config [charset] = explode ( ,, $this->config [charset] );
		
		$m = floor ( mt_rand ( 0, $this->config [master_num] - 1 ) );
		// 数据库读写是否分离
		if ($this->config [rw_separate]) {
			// 主从式采用读写分离
			if ($master)
				// 主服务器写入
				$r = $m;
			else {
				if (is_numeric ( $this->config [slave_no] )) { // 指定服务器读
					$r = $this->config [slave_no];
				} else {
					// 读操作连接从服务器
					$r = floor ( mt_rand ( $this->config [master_num], count ( $_config [hostname] ) - 1 ) ); // 每次随机连接的数据库
				}
			}
		} else {
			// 读写操作不区分服务器
			$r = floor ( mt_rand ( 0, count ( $_config [hostname] ) - 1 ) ); // 每次随机连接的数据库
		}
		
		if ($m != $r) {
			$db_master = array (
					username => isset ( $_config [username] [$m] ) ? $_config [username] [$m] : $_config [username] [0],
					password => isset ( $_config [password] [$m] ) ? $_config [password] [$m] : $_config [password] [0],
					hostname => isset ( $_config [hostname] [$m] ) ? $_config [hostname] [$m] : $_config [hostname] [0],
					hostport => isset ( $_config [hostport] [$m] ) ? $_config [hostport] [$m] : $_config [hostport] [0],
					database => isset ( $_config [database] [$m] ) ? $_config [database] [$m] : $_config [database] [0],
					dsn => isset ( $_config [dsn] [$m] ) ? $_config [dsn] [$m] : $_config [dsn] [0],
					charset => isset ( $_config [charset] [$m] ) ? $_config [charset] [$m] : $_config [charset] [0] 
			);
		}
		$db_config = array (
				username => isset ( $_config [username] [$r] ) ? $_config [username] [$r] : $_config [username] [0],
				password => isset ( $_config [password] [$r] ) ? $_config [password] [$r] : $_config [password] [0],
				hostname => isset ( $_config [hostname] [$r] ) ? $_config [hostname] [$r] : $_config [hostname] [0],
				hostport => isset ( $_config [hostport] [$r] ) ? $_config [hostport] [$r] : $_config [hostport] [0],
				database => isset ( $_config [database] [$r] ) ? $_config [database] [$r] : $_config [database] [0],
				dsn => isset ( $_config [dsn] [$r] ) ? $_config [dsn] [$r] : $_config [dsn] [0],
				charset => isset ( $_config [charset] [$r] ) ? $_config [charset] [$r] : $_config [charset] [0] 
		);
		return $this->connect ( $db_config, $r, $r == $m ? false : $db_master );
	}

其实就是通过传入initConnect来区分读写操作,并根据配置去连接操作。

今天分析就到这里了,其实与我们yy的基本一致。


ThinkPHP读写分离的内部实现
https://blog.puresai.com/2018/12/17/174/
作者
puresai
许可协议