Работа с MySQL кластером в Doctrine
В этой статье я хочу рассказать, как настроить Doctrine таким образом, чтобы использовать master-сервера для записи, а slave-сервера для чтения данных. Эта стратегия позволит добиться распределения нагрузки между серверами БД. Речь идет именно о нескольких серверах, на которых реплицируются базы, а не о ndbcluster, как некоторые могли бы подумать.
Итак, сначала мы должны сконфигурировать все дсотупные соединения в Doctrine.
1 2 3 4 5 6 7 8 9 10 11 |
$connections = array( 'master' => 'mysql://root:@master/dbname', 'slave_1' => 'mysql://root:@slave1/dbname', 'slave_2' => 'mysql://root:@slave2/dbname', 'slave_3' => 'mysql://root:@slave3/dbname', 'slave_4' => 'mysql://root:@slave4/dbname' ); foreach ($connections as $name => $dsn) { Doctrine_Manager::connection($dsn, $name); } |
Теперь, когда у нас есть один master-сервер, и 4 slave-сервера мы можем переписать классы Doctrine_Record и Doctrine_Query для добавления логики по разделению запросов для чтения и для записи. Все запросы на запись мы будем отправлять на master-сервер, а все запросы на чтение на случайно выбранный slave-сервер. См. статью про мульти-мастер репликацию
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class MyQuery extends Doctrine_Query { // Since php doesn't support late static binding in 5.2 we need to override // this method to instantiate a new MyQuery instead of Doctrine_Query public static function create($conn = null) { return new MyQuery($conn); } public function preQuery() { // If this is a select query then set connection to one of the slaves if ($this->getType() == Doctrine_Query::SELECT) { $this->_conn = Doctrine_Manager::getInstance()->getConnection('slave_' . rand(1, 4)); // All other queries are writes so they need to go to the master } else { $this->_conn = Doctrine_Manager::getInstance()->getConnection('master'); } } } |
Теперь DQL-запросы будут распределятсья между серверами, а что делать с сохранением записи? Мы сможем указать соединение дял записи, переписав класс Doctrine_Record, и используя его как базовый для наших моделей.
1 2 3 4 5 6 7 8 9 10 11 12 |
abstract class MyRecord extends Doctrine_Record { public function save(Doctrine_Connection $conn = null) { // If specific connection is not provided then lets force the connection // to be the master if ($conn === null) { $conn = Doctrine_Manager::getInstance()->getConnection('master'); } parent::save($conn); } } |
Отлично! Теперь запросы на чтение буду траспределены между slave-серверами, а на запись пойдут на master-сервер. Вот несколько примеров работы запросоов и моделей:
1 2 3 4 5 6 7 8 9 |
class User extends MyRecord { public function setTableDefinition() { $this->setTableName('user'); $this->hasColumn('username', 'string', 255, array('type' => 'string', 'length' => '255')); $this->hasColumn('password', 'string', 255, array('type' => 'string', 'length' => '255')); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// The save() method will happen on the master connection because it is a write $user = new User(); $user->username = 'jwage'; $user->password = 'changeme'; $user->save(); // This query goes to one of the slaves because it is a read $q = new MyQuery(); $q->from('User u'); $users = $q->execute(); print_r($users->toArray(true)); // This query goes to the master connection because it is a write $q = new MyQuery(); $q->delete('User') ->from('User u') ->execute(); |