记一次Yii2中Mysql断开重连引起的Invalid parameter number: no parameters were bound错误修复

因为公司系统改造需求,需要使用到微服务框架。因为历史原因,理所当然的想到了Workerman这一利器。
但是因为之前公司用的是Yii2,里面的Orm以及ActiveRecord不要太好用,因此也不想放弃。
所以这段时间利用业余时间搞了个yii2结合workerman的简陋框架出来,详见Github

Yii2结合workerman的很简单,只需以下两步:
1. 修改web/index.php 后一句,把$app->run(); 改成$app->init();
2. 在workerman原有的start.php的头部,引入上面的index.php文件

另外的数据协议以及数据验证自由发挥。
至此大功告成,立即拥有Workerman的高并发、稳定等特点,并且享受Yii2的大部分便利。

下面来说说遇到的问题

众所周知,Yii2原本是服务于php-fpm+nginx等架构的框架,并不能很好的利用mysql长连接的优点,并且不支持连接池。
而且也没有数据库连接意外断开(网络中断或者被Mysql服务器主动断开连接)之后的重试机制。
因此,还需要对Yii2的\yii\db\Command类进行改造,我们在项目根目录下新建lib文件夹,新增Command.php文件
该文件继承自\yii\db\Command类并重写excute以及queryInternal方法,使其支持重试机制,直接上代码


public function execute() { try { return parent::execute(); } catch (\yii\db\Exception $e) { if ($this->shouldRetry($e)) { return parent::execute(); } else { throw $e; } } } protected function queryInternal($method, $fetchMode = null) { try { return parent::queryInternal($method, $fetchMode); } catch (\yii\db\Exception $e) { if ($this->shouldRetry($e)) { return parent::queryInternal($method, $fetchMode); } throw $e; } } /** * 判断该数据库异常是否需要重试.一般情况下链接断开的错误才需要重试 * 2006: MySQL server has gone away * 2013: Lost connection to MySQL server during query * 但是实际使用中发现,由于Yii2对数据库异常进行了处理并封装成\yii\db\Exception异常 * 因此2006错误的错误码并不能在errorInfo中获取到,因此需要判断errorMsg内容 * @param \yii\db\Exception $ex * @return bool * @throws \yii\db\Exception */ private function shouldRetry(\yii\db\Exception $ex) { $errorMsg = $ex->getMessage(); $gone = strpos($errorMsg, 'MySQL server has gone away'); if ($gone === false && empty($ex->errorInfo) && !in_array($ex->errorInfo[1], [2006, 2013])) { return false; } $this->retry = true; $this->pdoStatement = null; $this->db->close(); $this->db->open(); return true; }

然后在数据库配置的地方新增配置:
因为commandClass并标注为废弃@deprecated since 2.0.14. Use [[$commandMap]] for precise configuration,所以用classMap代替

[
    'class' => 'yii\db\Connection',
    'dsn' => 'mysql:host=localhost;dbname=yii2_basic_tests',
    'username' => 'root',
    'password' => 'root',
    'charset' => 'utf8',
    'slaveConfig' => [
        'username' => 'root',
        'password' => 'root',
        'attributes' => [
            PDO::ATTR_TIMEOUT => 10,
            PDO::ATTR_PERSISTENT => true,
        ],
        'charset' => 'utf8',
    ],
    'slaves' => [
        ['dsn' => 'mysql:host=localhost;dbname=yii2_basic_tests'],
    ],
    'commandMap' => ['mysql' => 'app\lib\Command'],
];

本以为就能高枕无忧,其他很多文章也只做到这一步,然而并不能
在后续测试过程中发现,数据库重连机制之后会出现另一个错误Invalid parameter number: no parameters were bound

百思不得姐,于是继续爬搜索引擎,发现大多数都是讲怎么断开重连的文章,并没有提到这个内容.
直到发现了这篇Yii2 解决2006 MySQL server has gone away问题
原文讲了因为调用了bindPendingParams方法绑定了参数之后,$this->_pendingParams = []一句使绑定的参数置空,因此重连之后会出现上述错误。
并且由于$_bindParams是个私有属性,子类无法访问并修改,所以原作者直接把框架那句话给注释了。释了。了。
确实是一个好办法😁但是不利于代码的维护以及版本更新,不可取。
然后查看文件用到了$this->_pendingParams的地方,发现只有bindValue以及bindValues两个方法对其做了赋值操作,更重要的是,每次赋值给_bindParams属性时,都会同时赋值一份给$params属性,而且$params属性不是私有的并且也不会被清空,因此可以利用这一点来(xie)大(ji)做(hang)文(dai)章(ma).
只需要在数据库重连之后,重新绑定一次参数不就完事了,说干就干。
还是在lib\Command.php文件里面,重写bindPendingParams方法

    /**
     * 利用$this->retry属性,标记当前是否是数据库重连
     * 重写bindPendingParams方法,当当前是数据库重连之后重试的时候
     * 调用bindValues方法重新绑定一次参数.
     */
    protected function bindPendingParams()
    {
        if ($this->retry) {
            $this->retry = false;
            $this->bindValues($this->params);
        }
        parent::bindPendingParams();
    }

至此改造完成,运行起来试验一下数据库断开重连之后的效果,no params bound的错误不见了。

小技巧

并不需要在代码里面sleep(30)让数据库断开连接。只需要找到本机对数据库的进程,然后kill掉即可。

SELECT * FROM  `information_schema`.processlist WHERE HOST LIKE '192.168.2.19%';

发表评论

电子邮件地址不会被公开。 必填项已用*标注