4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PDOでコネクションロスト後にDBに再接続する

Posted at

要約

アプリケーションサーバが重くなって、DBにコネクションが溜まって、サービス全体がダウン、なんてことにならないように、DBコネクションにwait_timeoutを設定するのは良いことです。
しかし、PDOではコネクションが切れてしまったときに自動で再接続する手段が提供されていません。
そこで、PDOのラッパークラスを作って、再接続できるようにしました。

環境

  • PHP7.0以上
  • MySQL(MariaDB10.1)

ハマりどころ

PDOをラップして、PDO::exec()の時に例外が発生したらcachして、ロストコネクションだったら再接続する、っていうだけなら簡単なんですが、このやりかただとPDO::prepare()->execute()されたときにcatchできなくなります。
execute()PDOStatementのメソッドだからです。

解決策

PDOのラッパークラス自身にタイマーを持たせて、wait_timeoutが経過したら無駄にクエリを1回発行して例外を発生させることで、かならずラッパークラス自身で再接続するようにしました。
ダサいとか言わないで。

サンプルコード

<?php

class ReConnectablePdo
{
    /** @var \PDO $pdo */
    private $pdo;

    private $dsn;
    private $username;
    private $password;
    private $options;
    private $timeout;
    private $lastExecMicroTime;

    /**
     * ReConnectablePdo constructor.
     * @param string $dsn
     * @param string $username
     * @param string $password
     * @param array $options
     * @param int|null $timeout
     */
    public function __construct($dsn, $username, $password, $options, $timeout)
    {
        $this->dsn = $dsn;
        $this->username = $username;
        $this->password = $password;
        $this->options = $options;
        $this->timeout = $timeout;

        $this->connect();
    }

    /**
     * @param $statement
     * @return int
     * @throws RuntimeException
     */
    public function exec($statement)
    {
        return $this->callPDOMethod('exec', [$statement]);
    }

    // こんな感じで各メソッドをラップする

    /**
     * @param string $methodName
     * @param array $params
     * @return int|mixed
     * @throws RuntimeException|\Exception
     */
    private function callPDOMethod($methodName, $params = [])
    {
        $retried = false;
        while (true) {
            try {
                // PDOから直接DBにアクセスせずPDOStatement経由でアクセスする場合があるので
                // クラス内でもTimeoutを監視しておく
                // 前回実行時からTimeout以上経過していた場合は
                // PDO経由でダミーのクエリを発行し再接続を促す
                if ($this->timeout && $this->lastExecMicroTime + $this->timeout - 0.1 <= microtime(true)) {
                    $this->pdo->exec('SELECT 1');
                }
                $ret = call_user_func_array([$this->pdo, $methodName], $params);
                $this->lastExecMicroTime = microtime(true);
                return $ret;
            } catch (\Exception $exception) {
                // 2連続でロストコネクション、またはロストコネクション以外の場合は例外を投げる
                if ($retried || !self::causedByLostConnection($exception)) {
                    throw $exception;
                }
                $this->connect();
                $retried = true;
            }
        }
        // ここには来ない
        return 0;
    }

    /**
     * @return void
     */
    private function connect()
    {
        if (isset($this->pdo)) {
            unset($this->pdo);
        }

        $this->pdo = new \PDO($this->dsn, $this->username, $this->password, $this->options);

        if ($this->timeout) {
            $this->pdo->exec("SET wait_timeout = {$this->timeout}");
        }

        $this->lastExecMicroTime = microtime(true);
    }

    /**
     * @param \Exception $exception
     * @return bool
     */
    private static function causedByLostConnection(\Exception $exception)
    {
        $errorMessagePatterns = [
            'server has gone away',
            'no connection to the server',
            'Lost connection',
            'is dead or not enabled',
            'Error while sending',
            'decryption failed or bad record mac',
            'server closed the connection unexpectedly',
            'SSL connection has been closed unexpectedly',
            'Error writing data to the connection',
            'Resource deadlock avoided',
        ];

        $message = $exception->getMessage();

        foreach ($errorMessagePatterns as $pattern) {
            if (stripos($message, $pattern) !== false) {
                return true;
            }
        }
        return false;
    }
}

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?