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();
这里有几个问题数据库
\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,一般有两种方式框架
ActiveRecord
$user = new app\models\User(); $user->name = 'name'; $user->update();
// 查询类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抛出的异常码和异常信息, 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 = []
语句,保证从新执行时能够再次绑定参数。