I have a PHP application that has grown in size. The database used to be on a single master but we intend to change that with a fairly standard master/slave replication for performance and HA.
Since this app is read-heavy I would like to have the reads delegated to the slave replicas and writes going to the master.
The app is based on Zend Framework 1.1.10 and uses Zend_Db.
What would be my best strategy for getting this app to split reads and writes to the DB without refactoring the code too much? (I realize there would probably be some refactoring involved here).
P.S:
I have looked at MySQL Proxy and it seems it can transparently split reads and writes by sitting in between the DB server and the app, but I'm not sure about the performance issues using this in a production environment. Does anyone have experience with this?
As you said MySQlProxy can be a solution, but I personnaly never tested it out in production.
I use 2 Db connections in my code to split-out write and read requests. 80% of the usual tasks are done with the read connection. You could use the Zend_Application_Resource_Multidb to handle that (For me I've done this part long before and I simply store a second Db connection in the registry).
- First limit your user rights only on read operation and create another db user with write authorization.
- then track every write request in your code ("update", "insert", "delete" is a good start) and try to make all these calls with a dedicated helper.
- run your app and watch it crash, then fix problems :-)
It's easier when you think this problem in the beginning. For example:
- I usually have a Zend_Db_Table factory, taking a 'read' or 'write' parameter, and giving me a Singleton of the right Zend_Db_Table (a dual singleton, it I can have a read instance and a write instance). Then I only need to ensure I use the right initialized Zend_Db_Table when I use write access queries/operations. Notice that memory usage is far better when using Zend_Db_Table as singletons.
- I try to get all write operations in a TransactionHandler. I there I can check I use only objects linked with the right connection. Transactions are then managed on controllers, I never try to manage transaction in Database layers, all start/commit/rollback thinking is done on the controllers (or another conceptual layer, but not the DAO layer).
This last point, transactions, is important. If you want to manage transaction it's important to make the READ requests INSIDE the transaction, with the WRITE-enabled connection. As all reads done before the transaction should be considered as outdated, and if your database backend is doing implicits locks you'll have to make the read request to get the locks. If your database backend is not doing implicit reads then you'll have to perform the row locks in the transaction as well. And that mean you should'nt rely on the SELECT keyword to push that request on the read-only connection.
If you have a nice db layer usage in your application the change is not really hard to make. If you made chaotic things with your database/DAO layer then... it may be harder.
h2. Zend
I just have patched Zend PDO_MYSQL to separate read-write connections. For this you will need just specify additional parameters in applicaiton configs:
'databases' => array (
'gtf' => array(
'adapter' => 'PDO_MYSQL',
'params' => array(
'host' => 'read.com',
'host_write' => 'write-database-host.com',
'dbname' => 'database',
'username' => 'reader',
'password' => 'reader',
'username_write' => 'writer',
'password_write' => 'writer',
'charset' => 'utf8'
)
),
Here all "SELECT ..." queries will use host. And all other queries will use *host_write*. If host_write not specified, then all queries use host.
Patch:
diff --git a/Modules/Tools/Externals/Zend/Db/Adapter/Abstract.php b/Modules/Tools/Externals/Zend/Db/Adapter/Abstract.php
index 5ed3283..d6fccd6 100644
--- a/Modules/Tools/Externals/Zend/Db/Adapter/Abstract.php
+++ b/Modules/Tools/Externals/Zend/Db/Adapter/Abstract.php
@@ -85,6 +85,14 @@ abstract class Zend_Db_Adapter_Abstract
* @var object|resource|null
*/
protected $_connection = null;
+
+
+ /**
+ * Database connection
+ *
+ * @var object|resource|null
+ */
+ protected $_connection_write = null;
/**
* Specifies the case of column names retrieved in queries
@@ -299,10 +307,13 @@ abstract class Zend_Db_Adapter_Abstract
*
* @return object|resource|null
*/
- public function getConnection()
+ public function getConnection($read_only_connection = true)
{
$this->_connect();
- return $this->_connection;
+ if (!$read_only_connection && $this->_connection_write)
+ return $this->_connection_write;
+ else
+ return $this->_connection;
}
/**
diff --git a/Modules/Tools/Externals/Zend/Db/Adapter/Pdo/Abstract.php b/Modules/Tools/Externals/Zend/Db/Adapter/Pdo/Abstract.php
index d7f6d8a..ee63c59 100644
--- a/Modules/Tools/Externals/Zend/Db/Adapter/Pdo/Abstract.php
+++ b/Modules/Tools/Externals/Zend/Db/Adapter/Pdo/Abstract.php
@@ -57,7 +57,7 @@ abstract class Zend_Db_Adapter_Pdo_Abstract extends Zend_Db_Adapter_Abstract
*
* @return string
*/
- protected function _dsn()
+ protected function _dsn($write_mode = false)
{
// baseline of DSN parts
$dsn = $this->_config;
@@ -65,10 +65,15 @@ abstract class Zend_Db_Adapter_Pdo_Abstract extends Zend_Db_Adapter_Abstract
// don't pass the username, password, charset, persistent and driver_options in the DSN
unset($dsn['username']);
unset($dsn['password']);
+ unset($dsn['username_write']);
+ unset($dsn['password_write']);
unset($dsn['options']);
unset($dsn['charset']);
unset($dsn['persistent']);
unset($dsn['driver_options']);
+
+ if ($write_mode) $dsn['host'] = $dsn['host_write'];
+ unset($dsn['host_write']);
// use all remaining parts in the DSN
foreach ($dsn as $key => $val) {
@@ -91,9 +96,6 @@ abstract class Zend_Db_Adapter_Pdo_Abstract extends Zend_Db_Adapter_Abstract
return;
}
// get the dsn first, because some adapters alter the $_pdoType
$dsn = $this->_dsn();
+ if ($this->_config['host_write'])
+ $dsn_write = $this->_dsn(true);
// check for PDO extension
if (!extension_loaded('pdo')) {
/**
@@ -120,14 +122,28 @@ abstract class Zend_Db_Adapter_Pdo_Abstract extends Zend_Db_Adapter_Abstract
$this->_config['driver_options'][PDO::ATTR_PERSISTENT] = true;
}
try {
$this->_connection = new PDO(
- $dsn,
+ $dsn_read,
$this->_config['username'],
$this->_config['password'],
$this->_config['driver_options']
);
+ if ($this->_config['host_write']) {
+ $this->_connection_write = new PDO(
+ $dsn_write,
+ $this->_config['username_write'],
+ $this->_config['password_write'],
+ $this->_config['driver_options']
+ );
+ }
+
$this->_profiler->queryEnd($q);
// set the PDO connection to perform case-folding on array keys, or not
diff --git a/Modules/Tools/Externals/Zend/Db/Statement/Pdo.php b/Modules/Tools/Externals/Zend/Db/Statement/Pdo.php
index 8bd9f98..4ab81bf 100644
--- a/Modules/Tools/Externals/Zend/Db/Statement/Pdo.php
+++ b/Modules/Tools/Externals/Zend/Db/Statement/Pdo.php
@@ -61,8 +61,11 @@ class Zend_Db_Statement_Pdo extends Zend_Db_Statement implements IteratorAggrega
*/
protected function _prepare($sql)
{
+
+ $read_only_connection = preg_match("/^select/i", $sql);
+
try {
- $this->_stmt = $this->_adapter->getConnection()->prepare($sql);
+ $this->_stmt = $this->_adapter->getConnection($read_only_connection)->prepare($sql);
} catch (PDOException $e) {
require_once 'Zend/Db/Statement/Exception.php';
throw new Zend_Db_Statement_Exception($e->getMessage());
来源:https://stackoverflow.com/questions/6175645/read-write-splits-using-zend-db