Yii2 解決2006 MySQL server has gone away問題
Yii2版本 2.0.15.1
php后台任務經常包含多段sql,如果php腳本執行時間較長,或者sql執行時間較長,經常會碰到mysql斷連,報2006 MySQL server has gone away
錯誤。通常,mysql斷連了,重連數據庫就好了,但是在哪里執行重連呢?這是一個值得思考的問題。
手動重連
最直接的解決辦法,是在執行較長sql,或者腳本執行合適的時機,手動重連
\Yii::$app->db->close();
\Yii::$app->db->open();
這里有幾個問題
- sql執行時間不好判斷,容易受數據庫壓力的影響。
- 插入重連代碼的時機不好判斷,太頻繁的重連會影響性能。
- 盡管已經充分考慮到插入重連數據庫代碼的位置,但是依然有"失手"的可能,不能保證完全解決問題。
- 每個數據庫都需要充分考慮,例如
\Yii::$app->db1->close()
,代碼可復用性不高。
需要時重連
捕獲mysql斷連異常,在異常處理中重連數據庫,重新執行sql。
通常,使用php原生的PDO
類連接數據庫的操作步驟是
// 1. 連接數據庫
$pdo = new PDO();
// 2. 執行prepare
$stm = $pdo->prepare(sql);
// 3. 綁定參數
$stm->bindValue();
// 4. 執行
$stm->query();
$stm->exec();
在Yii2框架中執行sql,通常有兩種方式
- 使用
ActiveRecord
$user = new app\models\User();
$user->name = 'name';
$user->update();
- 拼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
句柄。
於是我們改寫這兩個方法,實現捕獲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
句柄,還是會報錯。
yii\db\Exception
是Yii實現的Mysql異常,幫我們解析了Mysql拋出的異常碼和異常信息, 2006
和2013
均是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\Connection
的commmandClass
屬性就能修改創建的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\Command
的queryInternal()
和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
的,所以只能注釋掉該語句了。
總結
- 重寫
yii\db\Command
類的queryInternal()
和execute()
方法,捕獲mysql斷連異常。 - 在
db.php
中增加commandClass
配置,使得生成的Command
類為我們重寫的子類。 - 注釋掉
yii\db\Connection
中bindPendingParams()
方法的$this->_pendingParams = []
語句,保證重新執行時可以再次綁定參數。