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]);
有了这个代码, 就不用再担心死锁问题了。 一旦你的代码中有可能造成死锁的代码, 运行的时候回立即抛出一个异常。
连接对象优化。
在编写协程化程序的时候, 我们应该尽量避免让一个协程从头到尾持有一个数据库连接. 这会让协程性能大打折扣,遵循以下几个标准重新设计数据库连接类:
- new DB 的时候并不会直接从 连接池中拿取连接.
- 只有在执行Sql的时候才去从连接池中拿取连接。 查询语句执行完后立即将连接交还给连接池.
- 只有在启动事务的时候, 对象才在整个事务生命周期期间长期持有连接, 事务提交或者回滚后立即将连接交还.
以上是关于PDO 连接池死锁现象分析的主要内容,如果未能解决你的问题,请参考以下文章
Python并发编程05/ 死锁/递归锁/信号量/GIL锁/进程池/线程池