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 = [] 语句,保证重新执行时可以再次绑定参数。
|