PDO 连接池死锁现象分析

本文介绍了一个关于Swoole协程环境下数据库连接池引发的死锁问题。通过对问题的深入分析,作者提出了一种解决方案来避免同一协程中并发使用的数据库连接导致的死锁现象,并对连接对象进行了优化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题起因

我在项目中对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. 只有在启动事务的时候, 对象才在整个事务生命周期期间长期持有连接, 事务提交或者回滚后立即将连接交还.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值