我有一个PHP应用程序,它的规模越来越大。数据库曾经在一个主机上,但为了提高性能和HA,我们打算用一个相当标准的主/从复制来改变这一点。
由于这个应用程序的读取量很大,我希望将读取委派给从属副本,并将写入委派给主副本。
该应用程序基于Zend Framework 1.1.10,并使用Zend_Db。
在不过多重构代码的情况下,让这个应用程序拆分对数据库的读写,我的最佳策略是什么?(我意识到这里可能会涉及到一些重构)。
p.S:
我看过MySQL Proxy,它似乎可以通过位于DB服务器和应用程序之间透明地划分读写,但我不确定在生产环境中使用它会带来什么性能问题。有人有这方面的经验吗?
正如您所说,MySQlProxy可以是一个解决方案,但我个人从未在生产中测试过它。
我在代码中使用2个Db连接来划分写和读请求。80%的常规任务都是通过读取连接完成的。您可以使用Zend_Application_Resource_Multidb来处理这一问题(对我来说,我早就完成了这一部分,我只是在注册表中存储第二个Db连接)。
- 首先,仅在上限制您的用户权限读取操作并创建另一个数据库具有写入授权的用户
- 然后跟踪中的每个写入请求您的代码("更新"、"插入"、,"删除"是一个良好的开端),并尝试使用专用的助手
- 运行你的应用程序并看着它崩溃,然后修复问题:-)
当你一开始就考虑这个问题时,它会更容易。例如:
- 我通常有一个Zend_Db_Table工厂,取一个"read"或"write"参数,并给我一个右Zend_Db_Table的Singleton(一个双Singleton,我可以有一个读实例和一个写实例)。然后,当我使用写访问查询/操作时,我只需要确保使用正确初始化的Zend_Db_Table。请注意,当使用Zend_Db_Table作为singleton时,内存使用率要好得多
- 我尝试在TransactionHandler中获取所有写操作。我在那里我可以检查我只使用与正确连接链接的对象。然后在控制器上管理事务,我从不尝试在数据库层中管理事务,所有的启动/提交/回滚思想都是在控制器(或另一个概念层,但不是DAO层)上完成的
最后一点,事务,很重要。如果您想管理事务,请在事务内部发出READ请求,并启用WRITE连接。由于在事务之前完成的所有读取都应该被视为过时,并且如果数据库后端正在执行implicits锁,则必须发出读取请求才能获得锁。如果您的数据库后端没有执行隐式读取,那么您还必须在事务中执行行锁定这意味着您不应该依赖SELECT关键字在只读连接上推送该请求
如果你的应用程序中有一个很好的数据库层使用,那么改变并不难。如果你用数据库/DAO层做了一些混乱的事情,那么。。。这可能更难。
h2。Zend
我刚刚修补了Zend PDO_MYSQL以分离读写连接。为此,您只需要在应用程序配置中指定附加参数:
'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'
)
),
在这里,所有"SELECT…"查询都将使用主机。所有其他查询都将使用*host_write*。如果未指定host_write,则所有查询都使用host。
补丁:
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());