Phalcon框架和Yaf類似,是一款用C實現的拓展級別的框架,不過其功能實現更加豐富,設計思路基於依賴注入、容器等方式,更符合現代框架思想。本文主要針對Phalcon框架數據庫層的讀寫分離進行說明,權當記錄。
前提准備
既然需要主從分離,那么數據庫連接至少得有兩個,即將主庫和從庫分別作為服務注冊到di容器,如下
// app/config/services.php use Phalcon\Db\Adapter\Pdo\Mysql; // 設置主庫 $di->setShared('dbMaster', function () { $params = [ 'host' => '127.0.0.1', 'username' => 'root', 'password' => 'root', 'dbname' => 'phalcon_test', 'charset' => 'utf8', 'port' => 3306, ]; return new Mysql($params); }); // 設置從庫 本地測試 和主庫用一個mysql實例即可 $di->setShared('dbSlave', function () { $params = [ 'host' => '127.0.0.1', 'username' => 'root', 'password' => 'root', 'dbname' => 'phalcon_test', 'charset' => 'utf8', 'port' => 3306, ]; return new Mysql($params); });
實現方法一
基於Model初始化時的讀寫分離設置
Model配置如下,例子是寫在每個Model的initialize
方法中,當然你也可以統一寫到BaseModel中,然后每個Model繼承即可。
// app/models/Users.php use Phalcon\Mvc\Model; class Users extends Model { /** * 設置Model主從切換 */ public function initialize() { // 寫操作 使用dbMaster連接 $this->setWriteConnectionService('dbMaster'); // 讀操作 使用dbSlave連接 $this->setReadConnectionService('dbSlave'); } /** * 表名 */ public function getSource() { return 'table_xxx'; } }
實現方法二
基於modelsManager實現的讀寫分離
1、首先需要自定義modelsManager實現,用於替換框架自帶的Manager
// app/library/ModelsManager.php use Phalcon\Mvc\Model\Manager; class ModelsManager extends Manager { /** * 讀操作 返回dbSlave * * @param \Phalcon\Mvc\ModelInterface $model * * @return \Phalcon\Db\Adapter\Pdo\Mysql */ public function getReadConnection(\Phalcon\Mvc\ModelInterface $model) { return $this->getDI()->get('dbSlave'); } /** * 寫操作 返回dbMaster * * @param \Phalcon\Mvc\ModelInterface $model * * @return \Phalcon\Db\Adapter\Pdo\Mysql */ public function getWriteConnection(\Phalcon\Mvc\ModelInterface $model) { return $this->getDI()->get('dbMaster'); } }
2、將自定義modelsManage注冊到di容器
// app/config/services.php use app/library/ModelsManager; /** * 設置自定義modelsManager */ $di->setShared( 'modelsManager', new ModelsManager() );
實現效果
按照上述方法實現后,你所用到的Phalcon實現的SQL查詢均可自動實現主從切換,包括
Query
、QueryBuilder
、AR
、PHQL
等。
當然,如果你某些情境下需要強制指定主庫或者從庫操作怎么辦,如下即可:
// di容器 $di = \Phalcon\Di\FactoryDefault::getDefault(); // 獲取主庫鏈接直接操作 $di->get('dbMaster')->execute('update table_xxx set age = 100 where id = 1'); // 獲取從庫鏈接直接操作 $di->get('dbSlave')->query('select * from table_xxx');
如何驗證
空口無憑,我怎么知道所有的讀操作都發生在了從庫上,所有的寫操作都發生在了主庫上;別擔心,每次查詢的時候寫個日志記錄一下不就完了。
需要更改最上面注冊dbMaster
和dbSlave
時的邏輯,如下:
// app/config/services.php use Phalcon\Db\Adapter\Pdo\Mysql; // 設置主庫 $di->setShared('dbMaster', function () { $params = [ 'host' => '127.0.0.1', 'username' => 'root', 'password' => 'root', 'dbname' => 'phalcon_test', 'charset' => 'utf8', 'port' => 3306, ]; $connection = new Mysql($params); // 記錄每次sql查詢日志 $logger = new \Phalcon\Logger\Adapter\File('/tmp/phalcon_sqls.log'); $eventsManager = new \Phalcon\Events\Manager(); // 注冊記錄sql的event $eventsManager->attach('db', function ($event, $connection) use ($logger) { ($event->getType() == 'beforeQuery') && $logger->log('Master: ' . $connection->getRealSQLStatement()); }); // 設置事件管理器 $connection->setEventsManager($eventsManager); return $connection; }); // 設置從庫 本地測試 和主庫用一個mysql實例即可 $di->setShared('dbSlave', function () { $params = [ 'host' => '127.0.0.1', 'username' => 'root', 'password' => 'root', 'dbname' => 'phalcon_test', 'charset' => 'utf8', 'port' => 3306, ]; $connection = new Mysql($params); // 記錄每次sql查詢日志 $logger = new \Phalcon\Logger\Adapter\File('/tmp/phalcon_sqls.log'); $eventsManager = new \Phalcon\Events\Manager(); // 注冊記錄sql的event $eventsManager->attach('db', function ($event, $connection) use ($logger) { ($event->getType() == 'beforeQuery') && $logger->log('Slave: ' . $connection->getRealSQLStatement()); }); // 設置事件管理器 $connection->setEventsManager($eventsManager); return $connection; });
完成之后查看/tmp/phalcon_sqls.log
日志,如下,即代表成功進行了分離
[Wed, 16 May 18 11:18:43 +0800][DEBUG] Slave: SELECT `table_xxx`.`id` FROM `table_xxx`
[Wed, 16 May 18 11:18:43 +0800][DEBUG] Master: UPDATE `table_xxx` SET `name` = 'xxx'
優劣比較
個人覺得兩種方法實現各有優劣,需要結合自己的業務進行選取.
- 使用方法一時,可以單獨設置每個Model使用的db鏈接,適用於多個庫中同名表操作的不同情境,更加靈活。
- 使用方法二時比較固定,直接使用Manager返回固定db鏈接,不再做其他處理邏輯,比較死板,但性能會高於方法一。
建議如果只是為了使用主從切換功能的話,方法二會更適合,性能會更高,畢竟實際業務可能沒有那么多切換庫的邏輯。
寫在最后
上述兩種方法可以同時存在,但優先級是方法二 > 方法一,即modelsManager
的優先級高於Model
中定義。本質原因是因為Model中執行SQL查詢時,也會向modelsManager索取db實例,框架源代碼文件如下
// phalcon/mvc/model.zep /** * Gets the connection used to read data for the model */ public function getReadConnection() -> <AdapterInterface> { var transaction; let transaction = <TransactionInterface> this->_transaction; if typeof transaction == "object" { return transaction->getConnection(); } return (<ManagerInterface> this->_modelsManager)->getReadConnection(this); } /** * Gets the connection used to write data to the model */ public function getWriteConnection() -> <AdapterInterface> { var transaction; let transaction = <TransactionInterface> this->_transaction; if typeof transaction == "object" { return transaction->getConnection(); } return (<ManagerInterface> this->_modelsManager)->getWriteConnection(this); }
可以看出讀寫Model進行查詢時db實例的獲取會通過modelsManager的getReadConnection
和getWriteConnection
方法實現,默認情況下Manager會返回Model注冊時的實例,即方法一的實現,但如果同時使用了方法二,會直接覆蓋原本Manager的實現,不會再使用Model中注冊的邏輯。