Symfony 4 + ECCUBE , làm thế nào để luân chuyển query giữa các node master slave
Mở đầu
Hắn nhiều người trong chúng ta có biết qua master slave node trong RDS , mặc định các framework PHP nổi tiếng như PHP , Symfony đều có hỗ trợ config master slave endpoint, để việc luân chuyển query được hiệu quả, các query WRITE sẽ được chuyển qua node WRITE, các query READ (SELECT) sẽ được chuyển qua node READ.
Vấn đề.
Và đây chính là lúc vấn đề bắt đầu 🙃🙃🙃🙃 . ECCUBE được viết dựa trên symfony 4, nhưng lại custom theo kiểu:
Tất cả request được wrap trong 1 câu transaction của query DB, dù request đó chỉ toàn query READ 👌👌
src/Eccube/EventListener/TransactionListener.php
/**
* Kernel request listener callback.
*
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
if (!$this->isEnabled) {
log_debug('Transaction Listener is disabled.');
return;
}
if (!$event->isMasterRequest()) {
return;
}
/** @var Connection $Connection */
$Connection = $this->em->getConnection();
if (!$Connection->isConnected()) {
$Connection->connect();
}
$Connection->setAutoCommit(false);
$Connection->setTransactionIsolation(TransactionIsolationLevel::READ_COMMITTED);
$this->em->beginTransaction();
log_debug('Begin Transaction.');
}
Và rồi câu chuyện lại đi xa hơn 🥲🥲🥲 , đối với doctrine2 (thư viện query được dùng trong Symfony ECCUBE, một ORM library tương tự Eloquent của Laravel), các query được viết trong 1 transaction default sẽ được chuyển sang node WRITE (vì nó coi đây là 1 query sắp có ghi). TOANG !!!!
/**
* Primary-Replica Connection
*
* Connection can be used with primary-replica setups.
*
* Important for the understanding of this connection should be how and when
* it picks the replica or primary.
*
* 1. Replica if primary was never picked before and ONLY if 'getWrappedConnection'
* or 'executeQuery' is used.
* 2. Primary picked when 'exec', 'executeUpdate', 'executeStatement', 'insert', 'delete', 'update', 'createSavepoint',
* 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or
* 'prepare' is called.
* 3. If Primary was picked once during the lifetime of the connection it will always get picked afterwards.
* 4. One replica connection is randomly picked ONCE during a request.
*
* ATTENTION: You can write to the replica with this connection if you execute a write query without
* opening up a transaction. For example:
*
* $conn = DriverManager::getConnection(...);
* $conn->executeQuery("DELETE FROM table");
*
* Be aware that Connection#executeQuery is a method specifically for READ
* operations only.
*
* Use Connection#executeStatement for any SQL statement that changes/updates
* state in the database (UPDATE, INSERT, DELETE or DDL statements).
*
Giải pháp
Ở đây ta cần custom lại class connection 1 chút, tạo 1 file CustomMasterSlaveConnection.php kế thừa MasterSlaveConnection, điểm quan trọng ở đây là method connect(). Chúng được override method connect
chọn node WRITE READ phù hợp, implement thêm 1 cờ để lock, trong TH này ta có thể force cho câu query đó chạy trên NODE nào luôn cũng dc.
<?php
namespace Customize\Doctrine;
use Doctrine\DBAL\Connections\MasterSlaveConnection;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\PDOConnection;
use InvalidArgumentException;
class CustomMasterSlaveConnection extends MasterSlaveConnection implements Connection
{
private $currentConnectionName = null;
protected $lock = false;
/**
* Check is slave node.
*/
public function isSlave()
{
return $this->currentConnectionName == 'replica';
}
public function getCurrentConnectionName()
{
return $this->currentConnectionName;
}
/**
* Lock change connection.
*/
public function lock()
{
$this->lock = true;
return $this;
}
/**
* Unlock change connection.
*/
public function unlock()
{
$this->lock = false;
return $this;
}
/**
* @param string|null $connectionName
*
* @return bool
*/
public function forceConnect($connectionName = null)
{
if ($connectionName !== 'master' && $connectionName !== 'slave') {
throw new InvalidArgumentException('Invalid option to connect(), only master or slave allowed.');
}
if ($connectionName === 'master') {
$connectionName = 'primary';
}
if ($connectionName === 'slave') {
$connectionName = 'replica';
}
if (isset($this->connections[$connectionName])) {
$this->_conn = $this->connections[$connectionName];
$this->currentConnectionName = $connectionName;
return false;
}
$this->connections[$connectionName] = $this->_conn = $this->connectTo($connectionName);
$this->currentConnectionName = $connectionName;
return true;
}
/**
* @param string|null $connectionName
*
* @return bool
*/
public function connect($connectionName = null)
{
if ($connectionName === 'master') {
$connectionName = 'primary';
}
if ($connectionName === 'slave') {
$connectionName = 'replica';
}
if ($this->lock) {
return false;
}
return $this->performConnect($connectionName);
}
}
Cách dùng thì khá dễ, chỉ cần lock() ở đầu phân đoạn bạn muốn dùng node READ, sau khi xong dùng hàm unlock() là dc
$conn = $this->entityManager->getConnection();
$conn->lock()->forceConnect('slave');
// do something query SELECT
// ...
$conn->unlock();
Kết
Hy vọng trên đây là 1 giải pháp giúp bạn có thể custom framework ECCUBE lựa chọn node READ WRITE phù hợp để đáp ứng khả năng chịu tải của hệ thống 😙😙😙.
Tks for reading !!! 😉😉😉
All rights reserved