前回において、Laravelの読み込みと書き込みのDBを自動使い分け機能により、プログラムの設定だけで簡単にDB負荷を緩和できることを知りました。今回は、DBのトランザクションにおいてその機能の振る舞いをチェックしてみます。

例えば、以前の個数管理での在庫数変更の以下の例を見てます。

$product = Product::find(1); // 複製のDBから読み込む
 
$product->update(['inventory' => $product->inventory - 1]); // マスターのDBで書き込む

上では、まず、商品の情報をDBから読み込み、そこから得る在庫数をもとに次の行で在庫数を変更します。
しかし、2つのDBを使用するとなると、最初の行は複製DBからの読み込みとなり、次の行ではマスターのDBにおいての書き込みとなります。

問題は、複製のDBはマスターDBとの時間差があるので、もしかしたら最初の行で読み込んだのは最新の在庫数ではないかもしれないません。つまりデータの整合性の問題となる可能性です。

そうなら、以下のようにトランザクションで囲んでみましょう。

DB::beginTransaction();
 
$product = Product::find(1);
 
$product->update(['inventory' => $product->inventory - 1]);
 
DB::commit();

しかし、トランザクションはマスターDB内だけでの作業の整合性を保証するものなので、同じ問題となるのでは?

Laravelのコードを深く追ってみます。

まず、LaravelのDB接続のクラスでは、マスターDBの$pdoと読み込み専用の$readPdoの2つのDB接続のための変数が宣言されています。

...
class Connection implements ConnectionInterface
{
    use DetectsDeadlocks,
        DetectsLostConnections,
        Concerns\ManagesTransactions;

    /**
     * The active PDO connection.
     *
     * @var \PDO|\Closure
     */
    protected $pdo;

    /**
     * The active PDO connection used for reads.
     *
     * @var \PDO|\Closure
     */
    protected $readPdo;
...

そして、同じクラス内で、読み込みDBの接続の取得は、以下のメソッドで定義されています。

    /**
     * Get the current PDO connection used for reading.
     *
     * @return \PDO
     */
    public function getReadPdo()
    {
        if ($this->transactions > 0) { //トランザクションがあると、マスターのDB接続を使用
            return $this->getPdo();
        }

        if ($this->recordsModified && $this->getConfig('sticky')) {
            return $this->getPdo();
        }

        if ($this->readPdo instanceof Closure) {
            return $this->readPdo = call_user_func($this->readPdo);
        }

        return $this->readPdo ?: $this->getPdo();
    }
...

トランザクションがあると、読み込み専用のDBではなく、マスターのDBが使われる条件文があります。つまり、トランザクションの中では、たとえDB実行文が読み込み(SELECT SQL文)であっても、読み込みのDB接続は使われずマスターDBしか使われないということです。

ちなみに、上のコードではstickyのときも、条件文で読み込みのDBの使用しないようになっています。これは以下のようにデータベースの設定で指定できます。そして、目的はDB更新後に、書き込みをしたマスターDBから値を読みたいときです。トランザクションと同様に、読み込み専用のDBからでは時間差の問題が生ずるからです。良く考えられていますね。

...
'mysql' => [
    'read' => [
        'host' => [
            '192.168.1.1',
            '196.168.1.2',
        ],
    ],
    'write' => [
        'host' => [
            '196.168.1.3',
         ],
    ],
    'sticky'    => true,
    'driver'    => 'mysql',
    'database'  => 'database',
    'username'  => 'root',
    'password'  => '',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
],
...

By khino