Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 535|回复: 0

Yii2 解决2006 MySQL server has gone away问题

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-4-21 15:39:31 | 显示全部楼层 |阅读模式

    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();
    

    这里有几个问题

    1. sql执行时间不好判断,容易受数据库压力的影响。
    2. 插入重连代码的时机不好判断,太频繁的重连会影响性能。
    3. 尽管已经充分考虑到插入重连数据库代码的位置,但是依然有"失手"的可能,不能保证完全解决问题。
    4. 每个数据库都需要充分考虑,例如\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,通常有两种方式

    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句柄。

    于是我们改写这两个方法,实现捕获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抛出的异常码和异常信息, 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 = []语句,保证重新执行时可以再次绑定参数。
    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-12-23 04:36 , Processed in 0.055600 second(s), 28 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表