Thinker

PDO之MySql持久化自动重连导致内存溢出

前言

最近项目需要一个常驻内存的脚本来执行队列程序,脚本完成后发现Mysql自动重连部分存在内存溢出,导致运行一段时间后,会超出PHP内存限制退出

排查

发现脚本存在内存溢出后排查了一遍代码,基本确认内存溢出在Mysql查询部分.
如果不确认也可以把不必要的模块去掉,从最基本的业务开始测,如果没有内存泄露,则继续增加模块,基本很快就能定位到内存泄露处的代码

解决

我使用的是PHP7.2,所以mysql查询部分使用了PDO,但是PDO并没有提供关闭连接的方法,只是说将连接connection赋值为Null,PHP便会进行垃圾回收,销毁连接.下面是PHP官方手册中的一段话

连接数据成功后,返回一个 PDO 类的实例给脚本,此连接在 PDO 对象的生存周期中保持活动。要想关闭连接,需要销毁对象以确保所有剩余到它的引用都被删除,可以赋一个 NULL 值给对象变量。如果不明确地这么做,PHP 在脚本结束时会自动关闭连接

然而事情没有这么简单,赋值NULL后PHP并没有将PDO实例回收,依然存在.因为还需要将PDOStatement同样赋值为NULL,否则PHP还是不会去执行垃圾回收.

修改后的代码

namespace app\library;

use \PDO;

class Mysql
{
    /** @var string 域名 */
    private $host = '127.0.0.1';

    /** @var int 端口 */
    private $port = 3306;

    /** @var string 数据库 */
    private $dataBase = 'test';

    /** @var string 用户名 */
    private $userName = 'root';

    /** @var string 密码 */
    private $password = 'root';

    /** @var PDO 数据库连接 */
    public $connection;

    /** @var Mysql 单例 */
    private static $instance;

    /** @var \PDOStatement */
    protected $PDOStatement;

    private function __construct()
    {
    }

    /**
     * 连接数据库
     */
    protected function connection():void
    {
        $this->connection = new \pdo(
            "mysql:host={$this->host}:{$this->port};dbname={$this->dataBase}",
            $this->userName,
            $this->password,
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING]
        );
        //设置PDO抛出异常
        $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }

    /**
     * 获取数据库连接
     * @return PDO
     */
    private function getConnection(): PDO
    {
        if ($this->connection instanceof PDO) {
            return $this->connection;
        }

        $this->connection();

        return $this->connection;
    }

    /**
     * 获取实例
     * @return Mysql
     */
    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new Mysql();
        }
        return self::$instance;
    }

    /**
     * 重置链接
     */
    protected function resetConnection():void
    {
        $this->connection   = null;
        //将此赋值操作屏蔽会造成内存溢出
        $this->PDOStatement = null;
    }

    /**
     * 重连
     * @return PDO
     */
    protected function reconnection(): PDO
    {
        $this->resetConnection();

        return $this->getConnection();
    }

    /**
     * 查询
     * @param string $sql   sql文本
     * @return mixed
     */
    public function query(string $sql)
    {
        try {
            $this->PDOStatement =  @$this->getConnection()->query($sql);
            return $this->PDOStatement->fetch();
        } catch (\PDOException $e) {
            if (!$this->isMysqlAlive()) {
                $this->PDOStatement =  $this->reconnection()->query($sql);
                return $this->PDOStatement->fetch();
            }
            throw new $e;
        }
    }

    /**
     * mysql连接连接是否有效
     * @return bool
     */
    protected function isMysqlAlive(): bool
    {
        $errorInfo = $this->connection->errorInfo();

        if ($errorInfo[1] != 2006 or $errorInfo[1] != 2013) {
            return false;
        }

        return true;
    }

    /**
     * 关闭数据库连接
     */
    public function close():void
    {
        $this->connection = null;
        $this->PDOStatement = null;
    }

    public function __destruct():void
    {
        $this->connection = null;
        $this->PDOStatement = null;
    }

    /**
     * 禁止clone
     * @throws \Exception
     */
    public function __clone()
    {
        throw new \Exception('deny clone');
    }
}

WIKI

posted @ 2019-10-08 14:02  ThinkerBlog  阅读(739)  评论(0编辑  收藏  举报