前言
CodeIgniter默认是不支持读、写分离的,网上流传的一般做法是在CI_Model层修改,但这有几个问题:
首先使用CodeIgniter的用户都是用过之后才发现不支持的,然后要修改大量的旧代码,产生的影响较多;
其次,在Model层修改,如果有代码在Controller操作数据库,将不能支持读、写分离(虽然在Controller直接操作数据库不是好方法);
最后,在CI_Model层的修改都要让用户去使用不同的数据库实例,如写用$this->write_db->query(),读用$this->read_db->query(),这样旧代码基本无法修改,因为之前的代码都是调用$this->db->query()方法的,要修改大量的旧代码。
所以,在CI_Model层修改不是很好的方案。因CI_Model和CI_Controller都使用了Controller类的db实例,所以我选择的方法是在Controller引入两个数据库连接,SQL查询时动态分析是否为SELECT语句,如果是则使用只读连接,其它使用写连接,来实现读、写分离,坏处是需要修改核心代码,对系统升级有一定副作用。
本次修改的环境是:
CodeIgniter 2.1.4
MySQL 5.1+
PHP 5.5.9
nginx 1.6.0
步骤一:修改application/config/database.php
数据库读、写连接参数的配置。
$active_record = TRUE;
if (defined('ENVIRONMENT'))
{
switch (ENVIRONMENT)
{
case 'development':
$db['default']['hostname'] = '127.0.0.1';
$db['default']['username'] = '';
$db['default']['password'] = '';
$db['default']['database'] = '';
$db['default']['dbdriver'] = 'mysqli';
$db['default']['dbprefix'] = '';
$db['default']['pconnect'] = FALSE;
$db['default']['db_debug'] = FALSE;
$db['default']['cache_on'] = FALSE;
$db['default']['cachedir'] = $_SERVER['DOCUMENT_ROOT'].'/cache/';
$db['default']['char_set'] = 'utf8';
$db['default']['dbcollat'] = 'utf8_general_ci';
$db['default']['swap_pre'] = '';
$db['default']['autoinit'] = TRUE;
$db['default']['stricton'] = FALSE;
$db['write']['hostname'] = '127.0.0.1';
$db['write']['username'] = '';
$db['write']['password'] = '';
$db['write']['database'] = '';
$db['write']['dbdriver'] = 'mysqli';
$db['write']['dbprefix'] = '';
$db['write']['pconnect'] = FALSE;
$db['write']['db_debug'] = FALSE;
$db['write']['cache_on'] = FALSE;
$db['write']['cachedir'] = $_SERVER['DOCUMENT_ROOT'].'/cache/';
$db['write']['char_set'] = 'utf8';
$db['write']['dbcollat'] = 'utf8_general_ci';
$db['write']['swap_pre'] = '';
$db['write']['autoinit'] = TRUE;
$db['write']['stricton'] = FALSE;
$db['crawl']['stricton'] = FALSE;
break;
case 'testing':
$db['default']['hostname'] = '127.0.0.1';
$db['default']['port'] = '3306';
$db['default']['username'] = '';
$db['default']['password'] = '';
$db['default']['database'] = '';
$db['default']['dbdriver'] = 'mysqli';
$db['default']['dbprefix'] = '';
$db['default']['pconnect'] = FALSE;
$db['default']['db_debug'] = FALSE;
$db['default']['cache_on'] = FALSE;
$db['default']['cachedir'] = $_SERVER['DOCUMENT_ROOT'].'/cache/';
$db['default']['char_set'] = 'utf8';
$db['default']['dbcollat'] = 'utf8_general_ci';
$db['default']['swap_pre'] = '';
$db['default']['autoinit'] = TRUE;
$db['default']['stricton'] = FALSE;
$db['write']['hostname'] = '';
$db['write']['port'] = '3306';
$db['write']['username'] = '';
$db['write']['password'] = '';
$db['write']['database'] = 'caishenquan';
$db['write']['dbdriver'] = 'mysqli';
$db['write']['dbprefix'] = '';
$db['write']['pconnect'] = FALSE;
$db['write']['db_debug'] = FALSE;
$db['write']['cache_on'] = FALSE;
$db['write']['cachedir'] = $_SERVER['DOCUMENT_ROOT'].'/cache/';
$db['write']['char_set'] = 'utf8';
$db['write']['dbcollat'] = 'utf8_general_ci';
$db['write']['swap_pre'] = '';
$db['write']['autoinit'] = TRUE;
$db['write']['stricton'] = FALSE;
break;
case 'production':
$db['default']['hostname'] = '';
$db['default']['username'] = '';
$db['default']['password'] = '';
$db['default']['database'] = '';
$db['default']['dbdriver'] = 'mysqli';
$db['default']['dbprefix'] = '';
$db['default']['pconnect'] = FALSE;
$db['default']['db_debug'] = FALSE;
$db['default']['cache_on'] = FALSE;
$db['default']['cachedir'] = $_SERVER['DOCUMENT_ROOT'].'/cache/';
$db['default']['char_set'] = 'utf8';
$db['default']['dbcollat'] = 'utf8_general_ci';
$db['default']['swap_pre'] = '';
$db['default']['autoinit'] = TRUE;
$db['default']['stricton'] = FALSE;
$db['write']['hostname'] = '';
$db['write']['username'] = '';
$db['write']['password'] = '';
$db['write']['database'] = '';
$db['write']['dbdriver'] = 'mysqli';
$db['write']['dbprefix'] = '';
$db['write']['pconnect'] = FALSE;
$db['write']['db_debug'] = FALSE;
$db['write']['cache_on'] = FALSE;
$db['write']['cachedir'] = $_SERVER['DOCUMENT_ROOT'].'/cache/';
$db['write']['char_set'] = 'utf8';
$db['write']['dbcollat'] = 'utf8_general_ci';
$db['write']['swap_pre'] = '';
$db['write']['autoinit'] = TRUE;
$db['write']['stricton'] = FALSE;
break;
default:
exit('The application environment is not set correctly.');
}
}
$active_group = 'default';
步骤二、修改 system/core/Controller.php
添加两数据库实例变量
var $db_write = null;
var $db_read = null;
步骤三、修改system/core/Load.php
修改数据库实例加载方法database(),并添加新方法get_write_dsn(),my_build_str()
public function database($params = '', $return = FALSE, $active_record = NULL)
{
// Grab the super object
$CI =& get_instance();
// Do we even need to load the database class?
if (class_exists('CI_DB') AND $return == FALSE AND $active_record == NULL AND isset($CI->db) AND is_object($CI->db))
{
return FALSE;
}
require_once(BASEPATH.'database/DB.php');
if ($return === TRUE)
{
$db_instance = DB($params, $active_record);
//非单独使用的实例,不走主从分离判断
$db_instance->is_single_instance = true;
return $db_instance;
}
// Initialize the db variable. Needed to prevent
// reference errors with some configurations
$CI->db = '';
// Load the DB class
//$CI->db =& DB($params, $active_record);
$CI->db =& DB($this->get_write_dsn($params),$active_record);
//读、写分离 by swingcoder@163.com
$CI->db_read = & DB($params, $active_record);
$CI->db_write = $CI->db;
//db实例需要主从分离
$CI->db_write->is_single_instance = false;
$CI->db_read->is_single_instance = false;
}
/**
* 获取数据库连接(写模式)dsn
* driver://username:password@hostname/database
* @author swingcoder@163.com
*/
function get_write_dsn($params)
{
if ( ! defined('ENVIRONMENT') OR ! file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/database.php'))
{
if ( ! file_exists($file_path = APPPATH.'config/database.php'))
{
show_error('[get_write_dsn]The configuration file database.php does not exist.');
}
}
include($file_path);
if ( ! isset($db) OR count($db) == 0)
{
show_error('[get_write_dsn]No database connection settings were found in the database config file.');
}
if( isset($db['write']))
{
$connection_arguments = $db['write'];
}
else
{
if( is_string($params) AND strpos($params, '://') === FALSE && isset($db[$params])){
$connection_arguments = $db[$params];
}else{
$connection_arguments = $db['default'];
}
}
$dsn_base = $connection_arguments['dbdriver'].'://'.$connection_arguments['username'].':'.$connection_arguments['password'].'@'.$connection_arguments['hostname'].'/'.$connection_arguments['database'];
unset($connection_arguments['dbdriver']);
unset($connection_arguments['username']);
unset($connection_arguments['password']);
unset($connection_arguments['hostname']);
unset($connection_arguments['database']);
$dsn_query = $this->my_build_str($connection_arguments);
return $dsn_base.'?'.$dsn_query;
}
function my_build_str($arr)
{
$str='';
foreach($arr as $key=>$val)
{
$str.=($key.'='.strval($val).'&');
}
$str = preg_replace('/&$/','',$str);
return $str;
}
步骤四、修改 system/database/DB_driver.php
添加对SQL的数据库连接选择路由功能。
添加实例变量
var $is_single_instance = false;//是否单独使用,不与Controller一起配合使用 by swingcoder@163.com
替换下述方法
function simple_query($sql)
{
//depend on database.php setting $db['local']['autoinit']
if ( ! $this->conn_id)
{
$this->initialize($sql);
}
if($this->is_single_instance){
//不读写分离
return $this->_execute($sql);
}else{
//读写分离
$CI =& get_instance();
if(strstr(strtolower($sql),"select"))
{
return $CI->db_read->_execute($sql);
}
else
{
//默认选择主库!这点很重要,如果有未知功能也可以保证数据完整
return $CI->db_write->_execute($sql);
}
}
}
步骤五、修改 system/core/CodeIngiter.php
添加关闭数据库连接功能
if (class_exists('CI_DB') AND isset($CI->db))
{
$CI->db->close();
//读写分离 by swingcoder@163.com
$CI->db_write->close();
$CI->db_read->close();
}
测试
读者可以写一个controller,观察$this->db_read 和 $this->db_write 实例的连接信息(具体php的调试方法参考笔者之前的博客介绍);或对一个主、从数据进行操作实际检验效果。
总结
此方案在Controller层修改,适应了在Model层和Controller层的数据库操作的读、写分离,旧代码基本不用修改,加入适当的调整,对连接池或多主多从连接的支持也是可以的,缺点是代码要修改核心功能,影响升级。
后记:
2015-9-8 发现返回数据库单独使用时,不能走主从分离判断。如下面的方法
$this->csq_db = $this->load->database( 'default', true );
此时数据库实例是独立使用的,并不会使用全局从Controller继承的$db_read,$db_write实例,所以此时不能使用这两个实例。通过添加DB_Driver->is_single_instance实例变量来判断,同时修改了DB_Driver->simple_query()方法。
<作者 朱淦 350050183@qq.com 2015.8.11>