讓CodeIgniter支持數據庫讀寫分離

前言

CodeIgniter默認是不支持讀、寫分離的,網上流傳的通常作法是在CI_Model層修改,但這有幾個問題:php

  1. 首先使用CodeIgniter的用戶都是用過以後才發現不支持的,而後要修改大量的舊代碼,產生的影響較多;mysql

  2. 其次,在Model層修改,若是有代碼在Controller操做數據庫,將不能支持讀、寫分離(雖然在Controller直接操做數據庫不是好方法);nginx

  3. 最後,在CI_Model層的修改都要讓用戶去使用不一樣的數據庫實例,如寫用$this->write_db->query(),讀用$this->read_db->query(),這樣舊代碼基本沒法修改,由於以前的代碼都是調用$this->db->query()方法的,要修改大量的舊代碼。git

因此,在CI_Model層修改不是很好的方案。因CI_Model和CI_Controller都使用了Controller類的db實例,因此我選擇的方法是在Controller引入兩個數據庫鏈接,SQL查詢時動態分析是否爲SELECT語句,若是是則使用只讀鏈接,其它使用寫鏈接,來實現讀、寫分離,壞處是須要修改核心代碼,對系統升級有必定反作用。sql

本次修改的環境是:數據庫

  • CodeIgniter 2.1.4app

  • MySQL 5.1+測試

  • PHP 5.5.9ui

  • nginx 1.6.0this

步驟一:修改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';


步驟2、修改 system/core/Controller.php

添加兩數據庫實例變量

var $db_write = null;
var $db_read = null;

步驟3、修改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;
   }

步驟4、修改 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);
           }
       }
}

步驟5、修改 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>

相關文章
相關標籤/搜索