Yii2 解決2006 MySQL server has gone away問題

Yii2 解決2006 MySQL server has gone away問題

Yii2版本 2.0.15.1php

php後臺任務常常包含多段sql,若是php腳本執行時間較長,或者sql執行時間較長,常常會碰到mysql斷連,報2006 MySQL server has gone away錯誤。一般,mysql斷連了,重連數據庫就行了,可是在哪裏執行重連呢?這是一個值得思考的問題。mysql

手動重連

最直接的解決辦法,是在執行較長sql,或者腳本執行合適的時機,手動重連sql

\Yii::$app->db->close();
\Yii::$app->db->open();

這裏有幾個問題數據庫

  1. sql執行時間很差判斷,容易受數據庫壓力的影響。
  2. 插入重連代碼的時機很差判斷,太頻繁的重連會影響性能。
  3. 儘管已經充分考慮到插入重連數據庫代碼的位置,可是依然有"失手"的可能,不能保證徹底解決問題。
  4. 每一個數據庫都須要充分考慮,例如\Yii::$app->db1->close(),代碼可複用性不高。

須要時重連

捕獲mysql斷連異常,在異常處理中重連數據庫,從新執行sql。yii2

一般,使用php原生的PDO類鏈接數據庫的操做步驟是app

// 1. 鏈接數據庫
$pdo = new PDO();

// 2. 執行prepare
$stm = $pdo->prepare(sql);

// 3. 綁定參數
$stm->bindValue();

// 4. 執行
$stm->query();
$stm->exec();

在Yii2框架中執行sql,一般有兩種方式框架

  1. 使用ActiveRecord
$user = new app\models\User();
$user->name = 'name';
$user->update();
  1. 拼sql
// 查詢類sql select
$sql = <<<EOL
select * from user where name = ':name' limit 1;
EOL;
\Yii::$app->db->createCommand($sql, [':name' => 'name'])->queryAll();

// 更新類sql insert, update, delete...
$sql = <<<EOL
update xm_user set name = 'name1' where name = ':name';
EOL;
\Yii::$app->db->createCommand($sql, [':name' => 'name'])->execute();

在Yii2中,sql的執行,都會調用yii\db\Connection類的createCommand()方法得到yii\db\Command實例。由yii\db\Command類的queryInternal()方法執行查詢類sql,execute()方法執行更新類sql。
這裏的yii\db\Connection相似於PDO類,表明數據庫鏈接, yii\db\Command類相似於PDOStatement類, 它的$pdoStatement屬性,保存生成的PDOStatement句柄。yii

因而咱們改寫這兩個方法,實現捕獲mysql斷連異常,重連數據庫。性能

use yii\db\Command;

class MysqlCommand extends Command
{
    public function __construct($config = [])
    {
        parent::__construct($config);
    }

    protected function queryInternal($method, $fetchMode = null)
    {
        try {
            return parent::queryInternal($method, $fetchMode);
        } catch (\yii\db\Exception $e) {
            if ($e->errorInfo[1] == 2006 || $e->errorInfo[1] == 2013) {
                echo '重連數據庫';
                $this->db->close();
                $this->db->open();
                $this->pdoStatement = null;
                return parent::queryInternal($method, $fetchMode);
            }
            throw $e;
        }
    }

    public function execute()
    {
        try {
            return parent::execute();
        } catch (\yii\db\Exception $e) {
            if ($e->errorInfo[1] == 2006 || $e->errorInfo[1] == 2013) {
                echo '重連數據庫';
                $this->db->close();
                $this->db->open();
                $this->pdoStatement = null;
                return parent::execute();
            }
            throw $e;
        }
    }
}

$this->pdoStatement = null是必要的,不然即便重連了數據庫,這裏再次執行queryInternal()execute()時,仍會使用原來生成的PDOStatement句柄,仍是會報錯。fetch

yii\db\Exception是Yii實現的Mysql異常,幫咱們解析了Mysql拋出的異常碼和異常信息, 20062013均是Mysql斷連異常碼。
捕獲到mysql異常後執行$this->db->close(),這裏的$db是使用createCommand()方法傳入的db實例, 因此咱們也無須要判斷db實例是哪個。

如何使得在調用createCommand()方法的時候,生成的使咱們重寫的子類MysqlCommand而不是默認的yii\db\Command呢?

閱讀代碼

public function createCommand($sql = null, $params = [])
{
    $driver = $this->getDriverName();
    $config = ['class' => 'yii\db\Command'];
    if ($this->commandClass !== $config['class']) {
        $config['class'] = $this->commandClass; // commandClass屬性能覆蓋默認的yii\db\Command類
    } elseif (isset($this->commandMap[$driver])) {
        $config = !is_array($this->commandMap[$driver]) ? ['class' => $this->commandMap[$driver]] : $this->commandMap[$driver];
    }
    $config['db'] = $this;
    $config['sql'] = $sql;
    /** @var Command $command */
    $command = Yii::createObject($config);
    return $command->bindValues($params);
}

咱們發現,只要修改yii\db\ConnectioncommmandClass屬性就能修改建立的Command類。
db.php配置中加上

'db' => [
    'class' => 'yii\db\Connection',
    'commandClass' => 'path\to\MysqlCommand', // 加上這一條配置
    'dsn' => '',
    'username' => '',
    'password' => '',
    'charset' => 'utf8',
],

這樣的配置,要保證使用Yii2提供的\Yii::createObject()方法建立對象才能生效。

作完以上的修改,在執行拼sql類的查詢且不綁定參數時沒有問題,可是在使用ActiveRecord類的方法或者有參數綁定時會報錯

SQLSTATE[HY093]: Invalid parameter number: no parameters were bound

說明咱們的sql沒有綁定參數。

爲何會出現這個問題?

仔細閱讀yii\db\CommandqueryInternal()execute()方法,發現他們都須要執行prepare()方法獲取PDOStatement實例, 調用bindPendingParams()方法綁定參數。

public function prepare($forRead = null)
{
    if ($this->pdoStatement) {
        $this->bindPendingParams(); // 綁定參數
        return;
    }

    $sql = $this->getSql();

    if ($this->db->getTransaction()) {
        // master is in a transaction. use the same connection.
        $forRead = false;
    }
    if ($forRead || $forRead === null && $this->db->getSchema()->isReadQuery($sql)) {
        $pdo = $this->db->getSlavePdo();
    } else {
        $pdo = $this->db->getMasterPdo();
    }

    try {
        $this->pdoStatement = $pdo->prepare($sql);
        $this->bindPendingParams(); // 綁定參數
    } catch (\Exception $e) {
        $message = $e->getMessage() . "\nFailed to prepare SQL: $sql";
        $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null;
        throw new Exception($message, $errorInfo, (int) $e->getCode(), $e);
    }
}

protected function bindPendingParams()
{
    foreach ($this->_pendingParams as $name => $value) {
        $this->pdoStatement->bindValue($name, $value[0], $value[1]);
    }
    $this->_pendingParams = []; // 調用一次以後就被置空了
}

這裏的$this->_pendingParams是在調用createCommand()方法時傳入的。
可是調用一次以後,執行了$this->_pendingParams = []將改屬性置空,因此當咱們重連數據庫以後,再執行到綁定參數這一步時,參數爲空,因此報錯。
本着軟件開發的"開閉原則",對擴展開發,對修改關閉,咱們應該重寫一個子類,修改掉這個方法,可是這個方法是private的,因此只能註釋掉該語句了。

總結

  1. 重寫yii\db\Command類的queryInternal()execute()方法,捕獲mysql斷連異常。
  2. db.php中增長commandClass配置,使得生成的Command類爲咱們重寫的子類。
  3. 註釋掉yii\db\ConnectionbindPendingParams()方法的$this->_pendingParams = []語句,保證從新執行時能夠再次綁定參數。
相關文章
相關標籤/搜索