PDO 连接池死锁现象分析

Posted bywayboy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PDO 连接池死锁现象分析相关的知识,希望对你有一定的参考价值。

问题起因

我在项目中对PDOPool进行了二次封装, 并写了一个简易的SqlBuilder, 目的还是为了简化开发. 从开发过程到项目上线都没发现问题。本以为一切顺利,然而直到一天,服务器升级一个功能需要重启的时候,意外出现了。系统启动的时候卡死~, 直觉告诉我,这是发生死锁了。然而,服务不能停,智能多次重启的方法,总算成功启动了一次。接下来是排查问题的过程。在程序入口多处加入日志打印后发现,通过模拟程序启动的时候的大量重连进来的现场后发现,程序卡死在 DbPool->get,原来真相在这。

问题分析

Swoole 协程数目是没有做限制的,只要有新的连接进来,就会启动一个协程进行处理,这个思路本身没有什么问题,然而, DBPool 是通过继承Channel 实现的,Channel的队列其实就是PDOmysql 连接。
正常情况下,我们每次使用一个数据库连接,用完后交还给连接池。当没有可用连接的时候 DbPool->get() 就会刮起当前协程,直到有可用的数据库连接再唤醒。 因此 无论多少协程,都会自觉排队等待PDOMysql连接处理。然而,当你一个“不小心”。 在一个协程内用到多个数据库连接到时候。是否发生死锁完全看系统繁忙程度了。 死锁发生过程如下:

首先,我们假定连接池有2个连接,并且有2个正在运行的协程。

# 协程 A                协程B                  
$db1 = $dbpool->get();          $db1 = $dbpool->get();        # 由于连接池中连接够,所以都是成功的
# do something                  # do something
$db2 = $dbpool->get();          $db2 = $dbpool->get();        # 连接池没有了连接, 死锁已成,后面的代码永远不会执行了.
# do something                  # do something                  
$dbpool->put($db2);             $dbpool->put($db2);           # 连接交换永远不会发生
$dbpool->put($db1);             $dbpool->put($db1);           # 连接交还永远不会发生

解决问题

既然问题已经找到了, 由于项目比较复杂,完全走一遍流程排查修改可能需要较长的时间, 这里在数据库驱动底层写一个死锁检查代码, 然后跑一遍完整接口测试. 基本上死锁问题便都找出来了。 具体实现如下:

class Db
    protected static $mark = [];        #保存协程ID 是否持有数据库连接的数组
    
    protected CheckDeadLock(int $cid)
        if($cid < 0) return;
        if(static::$cidMark[$cid] ?? false)
            throw new \\Exception("监测到可能的死锁! $cid, 同一个协程在同一时刻只允许持有一个数据库连接.");
        
    

    protected function getConn() : PDOProxy 
        if($this->conn !== null)
            return $this->conn;
        if(false !== ($this->cid = \\Swoole\\Coroutine::getCid()))
            static::CheckDeadLock($this->cid);
        
        return static::$pool[$this->name]->get();
    

    /**
     * @param $conn null|PDOProxy
     */
    protected function putConn(?PDOProxy $conn) 
        if($this->_trans_level > 0) 
            Log::write('错误! 您有忘记提交的事务,该次数据库操作将被丢弃!!!!', 'Db','ERROR');
            throw new Exception('错误! 您有忘记提交的事务, 该次数据库操作将被丢弃!!!!', 0);
        
        static::$pool[$this->name]->put($conn);
        # 死锁检测标记删除
        if(false !== $this->cid)
            unset(static::$cidMark[$this->cid]);
        
    

有了这个代码, 就不用再担心死锁问题了。 一旦你的代码中有可能造成死锁的代码, 运行的时候回立即抛出一个异常。

连接对象优化。

在编写协程化程序的时候, 我们应该尽量避免让一个协程从头到尾持有一个数据库连接. 这会让协程性能大打折扣,遵循以下几个标准重新设计数据库连接类:

  1. new DB 的时候并不会直接从 连接池中拿取连接.
  2. 只有在执行Sql的时候才去从连接池中拿取连接。 查询语句执行完后立即将连接交还给连接池.
  3. 只有在启动事务的时候, 对象才在整个事务生命周期期间长期持有连接, 事务提交或者回滚后立即将连接交还.

以上是关于PDO 连接池死锁现象分析的主要内容,如果未能解决你的问题,请参考以下文章

PDO 连接池死锁现象分析

PDO 连接池死锁现象分析

Python并发编程05/ 死锁/递归锁/信号量/GIL锁/进程池/线程池

死锁Lock锁等待唤醒机制线程组线程池定时器单例设计模式_DAY24

性能实战分析-问题分析

死锁编码及定位分析