背景
最近工作中有个项目,用 Phalcon 框架开发,涉及 RabbitMQ,所以自己写了个类来处理消息。
部署时用 Supervisor 监管,但发现一段时间没有业务触发就会出现 SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
的错误。
分析
为什么会出现这个错误?官方给出了解释 https://dev.mysql.com/doc/refman/5.7/en/gone-away.html
The most common reason for the MySQL server has gone away error is that the server timed out and closed the connection.
In this case, you normally get one of the following error codes (which one you get is operating system-dependent).
也就是说,出现 gone away,大部分是因为服务超时或断连了,而且错误码不一定是 2006(CR_SERVER_GONE_ERROR),也可能是 2013(CR_SERVER_LOST)。
常见的原因大致有:
- 运行线程被杀掉了。
- 关闭连接后又发出了SQL查询。
- 运行环境没有授权连接MySQL。
- 客户端的TCP/IP连接超时。
比如做了这样的设置mysql_options(..., MYSQL_OPT_READ_TIMEOUT,...)
或mysql_options(..., MYSQL_OPT_WRITE_TIMEOUT,...)
。 - wait_timeout,即,服务端主动关闭超时未交互的连接。
- 异常查询请求导致服务端主动关闭连接。
比如 超出了max_allowed_packet
变量设定的查询限制。
所以,大概率是 wait_timeout
导致的。
解决
用 show variables like '%timeout%';
查看 MySQL 的 wait_timeout
和 interactive_timeout
。
如果 my.ini 中没有配置,则默认 wait_timeout
为 28800,即,8小时。
为了测试,修改本地 MySQL 的 my.ini 配置:
1 | [mysqld] |
直接上代码(一段实现异常重连的示例代码):
1 | // 创建数据库连接 |
PDO::ATTR_ERRMODE
PDO 的错误报告方式有 PDO::ERRMODE_SILENT(静默)、PDO::ERRMODE_WARNING(引发 E_WARNING)、PDO::ERRMODE_EXCEPTION(抛出 Exception 异常)。
所以,需设置错误报告方式为 PDO::ERRMODE_EXCEPTION
,否则默认抛出 WARNING。
PDO::ATTR_EMULATE_PREPARES
与 prepare
细心的你可能会发现:gone away 异常是在 $sth->execute
处抛出的,为什么不是 $sth->prepare
呢?
PDO 默认启用预处理语句的模拟,即 PDO::ATTR_EMULATE_PREPARES
为 true
。
https://www.php.net/manual/zh/pdo.setattribute.php
PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句(如果为 true),或试着使用本地预处理语句(如果为 false)。如果驱动不能成功预处理当前查询,它将总是回到模拟预处理语句上。
而在此情况下,prepare 语句不会和数据库服务器交互。
https://www.php.net/manual/zh/pdo.prepare.php
模拟模式下的 prepare 语句不会和数据库服务器交互,所以
PDO::prepare()
不会检查语句。
所以,重连后重新执行预处理方式的SQL语句时,需要重新运行 prepare 来返回新的 PDOStatement 对象。
PDO::ATTR_PERSISTENT
当然,可以通过长连接来实现不超时断连。
PDO 提供了 PDO::ATTR_PERSISTENT
来设置建立的连接是否为长连接。
备注:PHP中实现的数据库长连接,实际上是通过复用与数据库建连的子进程来实现的,如果某个请求占用了子进程,则新的请求会触发创建新的子进程来与数据库建连。(注意区分传统意义上通过线程实现的数据库连接池)
更多了解可以参考官方文档:https://www.php.net/manual/zh/features.persistent-connections.php
这种方式虽然简洁,但没那么健壮,更好的方式还是捕获 gone away 异常并重新建连。
Phalcon 框架下实现重连
Phalcon 的数据库操作有很多种方式:原生、模型、PHQL。
所以要实现重连,我们只能在 Db Adapter 上实现。
通过继承 Phalcon\Db\Adapter\Pdo\Mysql
来扩展自己的 MySQL 逻辑:
1 | use Phalcon\Db\Adapter\Pdo\Mysql as BaseMysql; |
关于 PDO 中的 errorInfo
Phalcon\Db\Adapter\Pdo\AbstractPdo
中的 getErrorInfo
实际上是 PDO::errorInfo()
https://github.com/phalcon/cphalcon/blob/master/phalcon/Db/Adapter/Pdo/AbstractPdo.zep#L593
1 | public function getErrorInfo() |
而官方手册上有这么一段说明:
https://www.php.net/manual/zh/pdo.errorinfo.php
PDO::errorInfo() only retrieves error information for operations performed directly on the database handle. If you create a PDOStatement object through PDO::prepare() or PDO::query() and invoke an error on the statement handle, PDO::errorInfo() will not reflect the error from the statement handle. You must call PDOStatement::errorInfo() to return the error information for an operation performed on a particular statement handle.
也就是说,PDO::errorInfo()
只获取直接在数据库句柄上执行操作的错误信息。
如果错误是发生在通过 PDO::prepare()
或 PDO::query()
创建的 PDOStatement 对象句柄上,则必须用 PDOStatement::errorInfo()
来获取。
而我们很难在封装的 Phalcon 里获取 PDOStatement 对象,怎么办?
https://www.php.net/manual/zh/class.pdoexception.php#pdoexception.props.errorinfo
PDOException 的 errorInfo 包含了 PDO::errorInfo()
和 PDOStatement::errorInfo()
的情况,Nice!
Phalcon\Db\Adapter\Pdo\AbstractPdo
中的 executePrepared
Phalcon\Db\Adapter\Pdo\AbstractPdo
的 query
方法调用了自身的 executePrepared
方法
https://github.com/phalcon/cphalcon/blob/master/phalcon/Db/Adapter/Pdo/AbstractPdo.zep#L719
而 executePrepared
方法传入的 statement 是用 pdo->prepare(sqlStatement)
创建的
https://github.com/phalcon/cphalcon/blob/master/phalcon/Db/Adapter/Pdo/AbstractPdo.zep#L754
根据之前的梳理,我们知道,异常是在 statement->execute()
上抛出的,但重新执行 execute 必须创建新的 PDOStatement 对象。
1 | if ($fun[1] === 'executePrepared') { |
至此,问题搞定,写段代码来测试一下吧:
1 | $i = 0; |
几个问题
为什么 $dbh = null
就可以关闭 PDO 建立的 MySQL 连接?
https://www.php.net/manual/zh/pdo.connections.php
连接在 PDO 对象的生存周期中保持活动。
要想关闭连接,需要销毁对象以确保所有剩余到它的引用都被删除,可以赋一个 null 值给对象变量。
如果不明确地这么做,PHP 在脚本结束时会自动关闭连接。