В мире современных веб-приложений масштабируемость и производительность имеют первостепенное значение. По мере роста вашего приложения обработка растущего количества запросов к базе данных становится критической проблемой. Именно здесь в игру вступает концепция реплик чтения или записи, поскольку, распределяя операции базы данных между несколькими серверами баз данных, вы можете сократить время отклика и эффективно управлять большими нагрузками. Давайте углубимся в то, как реализация реплик чтения или записи в проекте Symfony может быть реализована с помощью пакета Doctrine.
Понимание концепции
Прежде чем углубиться в технические детали, давайте разберемся с концепцией реплик чтения или записи. В типичном веб-приложении существует два типа операций с базой данных: операции чтения (операторы SELECT) и операции записи (операторы INSERT, UPDATE, DELETE). Вместо того, чтобы обременять один сервер базы данных обоими типами операций, реплики чтения позволяют нам разгружать запросы чтения на отдельные серверы баз данных, распределяя таким образом нагрузку и повышая общую производительность. Кроме того, может быть полезно иметь экземпляры, доступные только для чтения, которые служат второстепенным целям например исследованиям, а не основным.
Выполнение
Следующие шаги предназначены для проекта Symfony с использованием Doctrine/doctrine-bundle.
Начнем с конфигурации доктрины ( config/packages/doctrine.yaml ):
when@prod:
doctrine:
dbal:
default_connection: default
connections:
default:
url: '%env(resolve:DATABASE_URL)%'
driver: pdo_pgsql
server_version: 15
replicas:
replica1:
url: '%env(resolve:DATABASE_RO_URL)%'
Как видите, в этом примере добавлена одна реплика, но вы можете добавить больше, Doctrine выбирает одну случайно в зависимости от вашего варианта использования и нагрузки базы данных. Важно отметить, что хотя экземпляр реплики будет предпочтительнее для операций чтения, ваш основной экземпляр также будет выполнять их, если он был выбран ранее во время существования соединения обслуживающего запрос. Если вы хотите выделить основной экземпляр только для операций записи, соединение по умолчанию doctrine.yaml должно иметь дополнительную настройку keep_replica: true чтобы сохранить фактическую реплику, а не использовать основной экземпляр в качестве реплики, и после выполнения любых операций записи вам нужно будет вручную обеспечить подключение к реплики.
$connection = $this->getEntityManager()->getConnection();
if ($connection instanceof PrimaryReadReplicaConnection) {
$connection->ensureConnectedToReplica();
}
Обратите внимание на instanceof условие — необходимо убедиться, что вы выполняете принудительное первичное подключение только в случае производственной среды, где собственно настроена реплика. В среде разработки $connection будет экземпляром Doctrine\DBAL\Connection.
Кроме того, в doctrine.yaml конфигурации DATABASE_URL ссылается на основной экземпляр вашей базы данных и DATABASE_RO_URL относится к реплике, доступной только для чтения — убедитесь, что эти переменные определены в вашем env файле.
DATABASE_URL="postgresql://user:password@read-write-database-instance:5432/table?charset=utf8"
DATABASE_RO_URL="postgresql://user:password@read-database-instance:5432/table?charset= utf8"
В довольно простых приложениях этой конфигурации должно быть достаточно, и при использовании методов Doctrine Repository или QueryBuilder она будет работать из коробки. Однако иногда вам может потребоваться выбрать SQL-запросы, когда вы очень заботитесь о производительности или просто предпочитаете не тратить время на реализацию собственных сложных функций DQL. При выполнении SQL-запросов необходимо помнить, что executeQuery этот метод предназначен только для операций READ. Код, приведенный ниже, не будет работать, поскольку Doctrine выберет реплику, доступную только для чтения, и в конечном итоге проксирует ошибки механизма БД, заявляя, что он не может выполнять операции записи.
$sql = <<<SQL
UPDATE smart_contract SET uaw = 0
FROM dapp_chain dc
WHERE dc.id = smart_contract.dapp_chain_id AND dc.chain_id = :chainId;
SQL;
$this->getEntityManager()->getConnection()->executeQuery($sql, [
'chainId' => $chainId,
]);
Для решения этой проблемы есть как минимум два возможных решения:
- Рекомендуется. Предпочитайте executeStatement любой executeQuery оператор SQL, который изменяет или обновляет состояние любой записи в базе данных. Использование executeStatement, и других операций записи или транзакций заставляет Doctrine выбирать первичное соединение insert.deletebeginTransactioncommitrollback.
- В некоторых случаях вам может потребоваться вручную заставить Doctrine Connection использовать основной экземпляр. Обновленный пример сценария может выглядеть так:
$sql = <<<SQL
UPDATE smart_contract SET uaw = 0
FROM dapp_chain dc
WHERE dc.id = smart_contract.dapp_chain_id AND dc.chain_id = :chainId;
SQL;
$connection = $this->getEntityManager()->getConnection();
if ($connection instanceof PrimaryReadReplicaConnection) {
$connection->ensureConnectedToPrimary();
}
$connection->executeQuery($sql, ['chainId' => $chainId]);
Заключение
В постоянно меняющемся мире веб-приложений оптимизация производительности — это непрерывный процесс. Интеграция реплик чтения или записи в ваш проект Symfony с использованием пакета Doctrine может изменить правила игры в этом стремлении. Стратегически распределяя рабочую нагрузку базы данных, вы можете повысить скорость реагирования и масштабируемость вашего приложения, гарантируя бесперебойную работу пользователей даже при резком росте трафика.