Yii framework 1.1.x 版本中,官方没有对数据库读写分离提供支持(在2.0版本中已支持),所以我们需要自己实现它。

Yii 扩展社区中 CJ 提供了一个扩展DbConnectionMan来实现数据库的读写分离,但它只能支持在调用 createCommand($sql) 方法,直接传入预编译的SQL语句时有效。也就是说,在使用 ActiveRecord:

Test::model()->find()

或者:

Yii::app()->db->createCommand('select id from test');

中有效,而对于:

Yii::app()->db->createCommand()
->select()
->from('test')
->query()

则是无效的。

因为第三种方式是由CDbCommand生成的SQL语句,并直接使用了CDbCommand::prepare(),而在DbConnectionMan中实现的分离操作是在CDbConnection中对接收到的SQL语句做判断后分离的连接。

如果我们把数据库分离操作移到CDbCOmmand::prepare()中(Yii 2.0版本中的读写分离就是在Command::prepar()中分离的),就可以解决这个问题,但由于框架中没有提供CDbCommand::$_statementCDbCommand::$_paramLog的访问权限,所以我们暂时无法重载CDbCommand::prepare()来实现分离操作。为此,我向Yii framework开发组提交了一个pull request,希望他们能接受。详见:#3647

##mysqlnd_ms支持

这里,我们使用php的原生扩展mysqlnd_ms在Yii framework中实现读写分离。

由于Yii使用PDO与MySQL数据库进行交互,而如果要在分离中让mysqlnd_ms能够自动的感知到是否是在进行事务操作,就需要对PDOATTR_AUTOCOMMIT操作。

  • 事务开始时关闭自动提交 PDO::ATTR_AUTOCOMMIT = false
  • 事务提交/回滚后重新开启自动提交 PDO::ATTR_AUTOCOMMIT = true

在Yii中,通常使用Yii::app()->db->beginTransaction()来开启一个事务,而默认情况下PDO::ATTR_AUTOCOMMIT是开启的。所以如果想不去一个一个修改代码,就来重写CDbConnection::beginTransaction()吧。

另外,要在提交/回滚后又自动开启自动提交,那就需要重写CDbTransaction::commit()CDbTransaction::rollback()方法。

CDbConnection::beginTransaction()方法的源码如下:

public function beginTransaction()
{
    Yii::trace('Starting transaction','system.db.CDbConnection');
    $this->setActive(true);
    $this->_pdo->beginTransaction();
    return $this->_transaction=new CDbTransaction($this);
}

其中$this->_pdo$this->_transactionprivate 属性,我们要重写CDbConnection::beginTransaction()肯定需要访问到这两个属性才行。

$this->_pdoCDbConnection::getPdoInstance()等效,这个没问题。

$this->_transaction稍微麻烦点,框架只提供了CDbConnection::getCurrentTransaction()让我们得到它,并没有提供一个CDbConnection::setCurrentTransaction()来让我们「调戏」它。所以我在 #3647 中同时也提交了一个CDbConnection::setCurrentTransaction()用于支持覆盖$this->_transaction属性。

那么,我们假设CDbConnection::setCurrentTransaction()已经存在了,现在我们开始重写我们自己的CDbConnectionCDbTransaction类:

我们定义一个类叫:MyDbConnection

<?php

class MyDbConnection extends CDbConnection
{
	public function beginTransaction()
	{
		Yii::trace('Starting transaction','system.db.CDbConnection');
    	$this->setActive(true);
    	$this->getPdoInstance()->beginTransaction();
    	$this->setAutoCommit(false); // 注意这里,关闭了自动提交,这是我们重写beginTransaction的目的。
    	$this->setCurrentTransaction(new MyDbTransaction($this));

    	return $this->getCurrentTransaction();

	}
}

然后我们需要重写 CDbTransaction 类来使得在提交和回滚时自动开启自动提交。 我们定义一个类叫:MyDbTransaction

<?php

class MyDbTransaction extends CDbTransaction
{
	public function commit()
	{
		parent::commit();
		$this->getConnection()->setAutoCommit(true);
	}
	
	public function rollback()
	{
		$this->getConnection()->setAutoCommit(true);
	}
}

这样一来,我们就可以在不改变现有业务代码的基础上使用mysqlnd_ms进行数据库读写分离了。

别忘了在mysqlnd_ms的配置文件里面添加上 "trx_stickiness": "master"设置