Hi,
this is a little proof of concept i did to enable some rudimentary application-level read/write-splitting in yii.
I first started with MySQLProxy, but also the contained rw-splitting script has a load balancer which doesn’t ensure that read querys go against a slave.
What’s all this good for?
I had a pretty simple issue: because of a really poor bandwidth i needed one instance of my application to run on a local network while the other runs on an internet server. The master db is located on this internet server and replicated to a local network slave using mysqls master-slave-replication.
There is a lot of data that is read from db while updates and insert are not so weightily, so every select-statement (for list views) that goes against the master could cost up to 30 seconds - that’s bad.
First of all it isn’t good practise to modify core files - i know - but i didn’t find another way because this needs to be located at the root of yiis db layer.
Comment: all this was done using yii 1.0.
1. CDbConnection
I’ve derived a class called CDbConnectionRwMaster from CDbConnection to add two attributes to the master-db-connection. _enableRwSplitting to enable/disable splitting and _slaves which holds the list of slave-connections for this master-connection.
<?php
/**
 * CDbConnectionRwMaster class file
 * Master db connection.
 */
class CDbConnectionRwMaster extends CDbConnection
{
    /**
     * bool Flag to enable read-write-splitting for this connection. Defaults to FALSE.
    */
    private $_enableRwSplitting = false;
   
    /**
     * Array Names of read-only slave connections for this master.
     * Example: $slaves = array('db_slave1', 'db_slave2', 'anotherSlaveConnection');
    */
    private $_slaves = array();
   
    public function getEnableRwSplitting()
    {
        return $this->_enableRwSplitting;
    }
   
    public function setEnableRwSplitting($value)
    {
        $this->_enableRwSplitting = (bool)$value;
    }
   
    public function getSlaves()
    {
        return $this->_slaves;
    }
   
    public function setSlaves($value)
    {
        if(is_array($value))
            $this->_slaves = $value;
        else {
            $this->_slaves = array();
            $this->_slaves[] = $value;
        }
    }   
}
?>
[b]
- CDbCommand[/b]
This is where we grub into yiis guts.
Here is a replacement of yiis private function queryInternal() located in CDbCommand.php:
private function queryInternal($method,$mode,$disableSplitting=false)
    {
        $lastConnection = $usedConnection = $this->getConnection();
        $is_select = $hasBadPattern = false;
       
        // $retry is just used to collect needed and evaluated statements while code is executed, otherwise
        // the IF-statement to check if we should fallback to the master is so damn long.
        $retry = true;
        // The bad patterns are searched for inside the sql query because the db connection
        // must not be changed to a slave if any of these sql commands is found
        $badPatterns = array('SQL_CALC_FOUND_ROWS','FOUND_ROWS','LAST_INSERT_ID','ROW_COUNT');
       
        // Application specific bad patterns, like non-splitable tables and so on
        $customBadPatterns = array();
       
        // Lets start when read-write-splitting is enabled local and global
        if(($retry &= ($disableSplitting===false)) && ($retry &= ($usedConnection->enableRwSplitting===true)))
        {
            if($retry &= ($usedConnection->getCurrentTransaction() == null))  // We are not in a transaction
            {
                $string = strtoupper(ltrim($this->getText()));
                if(strpos($string,'SELECT')===0 || strpos($string,'SHOW')===0) // Its a SELECT or SHOW query
                {
                    $retry &= $is_select = true;
                    $searchPatterns = array_merge($badPatterns,$customBadPatterns); // Merge standard and custom patterns
                   
                    // Search query string for bad words
                    foreach($searchPatterns as $pattern)
                    {
                        if(strpos($string,$badPattern)!==false) {
                            // A bad word was found inside the sql query
                            $hasBadPattern = true;
                            break;
                        }
                    }
                   
                    if($hasBadPattern===false)
                    {
                        $maxSlaves = count($usedConnection->slaves); // Count available slave connections
                        if($maxSlaves >= 1) {
                            $slaveConnection = $usedConnection->slaves[rand(0,$maxSlaves-1)]; // Pick a slave - very poor till now
                            if(Yii::app()->{$slaveConnection}->active != true)
                                Yii::app()->{$slaveConnection}->active = true; // Activate connection if necessary
                            $usedConnection = Yii::app()->{$slaveConnection};
                            Yii::trace('RW-splitting: Switching connection to SLAVE: '.Yii::app()->{$slaveConnection}->connectionString,'system.db.CDbCommand');
                        }
                    }
                    else
                        Yii::trace('RW-splitting: Keeping connection to '.$usedConnection->connectionString.' because of pattern detected','system.db.CDbCommand');
                }
            }
            else
                Yii::trace('RW-splitting: Keeping connection to '.$usedConnection->connectionString.' because of TRANSACTION','system.db.CDbCommand');
        }
        else
            Yii::trace('RW-splitting: Keeping connection - splitting is (temporarily) disabled for '.$usedConnection->connectionString,'system.db.CDbCommand');
   
        $this->_connection = $usedConnection;
   
        $params=$usedConnection->enableParamLogging && !empty($this->_params) ? '. Bind with parameter ' . implode(', ',$this->_params) : '';
        Yii::trace('Querying SQL: '.$this->getText().'; ['.$params.$usedConnection->connectionString.']','system.db.CDbCommand');
        try
        {
            if($usedConnection->enableProfiling)
                Yii::beginProfile('system.db.CDbCommand.query('.$this->getText().')  ['.$usedConnection->connectionString.']','system.db.CDbCommand.query');
            if($this->_statement instanceof PDOStatement)
                $this->_statement->execute();
            else
                $this->_statement=$usedConnection->getPdoInstance()->query($this->getText());
            if($method==='')
            {
                $result=new CDbDataReader($this);
                $n = $result->getColumnCount();
            }
            else
            {
                $result=$this->_statement->{$method}($mode);
                $n = $this->_statement->rowCount();
                $this->_statement->closeCursor();
            }
            /*
            * A simple master-slave replication doesnt ensure that both are consistent.
            * So after an INSERT maybe the slave doesnt yet have this record so we will take a look at the master-db
            */
            //if($n<=0 && $is_select && !$disableSplitting && ($usedConnection != $lastConnection ) && $this->getConnection()->enableRwSplitting && ($this->getConnection()->getCurrentTransaction() == null))
            // Shorter:
            if( $n<=0 && $retry && ($usedConnection != $lastConnection ) )
            {
                Yii::trace('RW-splitting: Empty result - falling back to MASTER: '.$this->getConnection()->connectionString,'system.db.CDbCommand');
               
                $this->_connection = $lastConnection;
               
                // PDOStatement is bound to the connection so we need to rebuild everything,
                // just changing $this->_connection wouldnt do the job
                if($this->_statement instanceof PDOStatement)
                {
                    $this->_statement = null;
                    foreach($this->_saveParams as $param)
                        $this->bindParam($params[0],$params[1],$params[2],$params[3]);
                    foreach($this->_saveValues as $value)
                        $this->bindValue($value[0],$value[1],$value[2]);
                }
                else
                    $this->_statement = null;
               
                // Retry by calling this function recursive but of course WITHOUT rw-splitting
                // because we want to fall back to the master
                return $this->queryInternal($method,$mode,true);
            }
           
            if($usedConnection->enableProfiling)
                Yii::endProfile('system.db.CDbCommand.query('.$this->getText().')','system.db.CDbCommand.query');
            return $result;
        }
        catch(Exception $e)
        {
            if($usedConnection->enableProfiling)
                Yii::endProfile('system.db.CDbCommand.query('.$this->getText().')','system.db.CDbCommand.query');
            Yii::log('Error in querying SQL: '.$this->getText().$params,CLogger::LEVEL_ERROR,'system.db.CDbCommand');
            throw new CDbException(Yii::t('yii','CDbCommand failed to execute the SQL statement: {error}',
                array('{error}'=>$e->getMessage())));
        }
    }
As you can see there are a lot of tracing messages in there to take a look at the behavior on runtime.
Beside the standard rw-splitting there is also a ‘master-fallback’ which requests a record from the master if the slave returned an empty result. This isn’t optimal yet but if the user INSERTs a record and the replication isn’t done yet the user would see a “there is not X with this id…” kind of message.
After adding the CDbConnectionRwMaster.php to /framework/db/ and changing the queryInternal() function of CDbCommand.php you can setup db connections in your main.php as following:
        'db'=>array(
            'class'=>'system.db.CDbConnectionRwMaster',
            'connectionString'=>'mysql:host=host1;dbname=myDB',
            'charset'=>'utf8',
            'username'=>'user',
            'password'=>'password',
            'slaves'=>array('db_readonly1','db_readonly2'), // List of read-only slaves
            'enableRwSplitting'=>true, // Enable read-write-splitting for this connection
        ),
        'db_readonly1'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'mysql:host=host2;dbname=myDB',
            'charset'=>'utf8',
            'username'=>'user',
            'password'=>'password',
        ),
        'db_readonly2'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'mysql:host=hostX;dbname=myDB',
            'charset'=>'utf8',
            'username'=>'user',
            'password'=>'password',
        ),
As mentioned above this is only a proof of concept and not ready to use in productive environments.
Don’t know but maybe this could be useful for someone. 
Best regards,
yoshi