代码 PDO 持久连接的状态残留:跨请求的事务、游标与预处理语句怎么排查

2026-10-30 21:09:35

PDO 持久连接的状态残留:跨请求的事务、游标与预处理语句怎么排查

PDO::ATTR_PERSISTENT 让连接在同一进程(FPM worker)内跨请求复用。省下的是握手开销,代价是:事务状态、预处理语句、游标和会话状态都越过了请求边界,下一个请求拿到的不是一个干净的连接。下面按状态可能残留的位置逐项拆。

事务不属于任何单个实例

修复项 GH-23962 描述得比较直接:销毁一个持久 PDO 实例,会回滚另一个实例仍在用的事务。因为底层是同一条连接,事务是驱动层状态,不随 PHP 对象走。

PDO::inTransaction() 报的也是驱动层状态,不是应用自己维护的标志位。复用连接时它返回 true,说明上一个请求留下的事务没结束,此时执行任何语句都在别人的事务里。

失败执行后旧结果还在

PDOStatement::execute() 失败后,上一次执行的结果集仍然可读。调用方只 fetch 不检查返回值,就会拿到上一轮的陈旧数据继续处理,表现成“数据对不上”而不是报错。

execute() 期间操作同一个 statement

绑定参数的 __toString() 在 execute() 过程中被调用;如果这里再去调 bindValue()、execute() 或 closeCursor(),就会操作正在执行的语句,构成 use-after-free。__toString() 里只做字符串转换,不要回调任何 statement 方法。

ATTR_STATEMENT_CLASS 的两种失败

PDO::setAttribute() 可能装上一个构造参数会被拒绝的 PDO::ATTR_STATEMENT_CLASS。另一个更难判断:constructor_args 类型错误时,报错永远是 array given,与传进去的实际值无关。看到这条信息,不要据此推断参数类型,自己检查传入结构。

绑定相关的泄漏与越界

  • bindParam() 和 bindColumn() 会泄漏 driver options 的值。
  • bindColumn() 会给不在结果集里的列名注册绑定。
  • getColumnMeta() 传无效列索引会越界读。
  • setFetchMode() 的变更不是原子应用的,中途读可能拿到半套配置。
  • PDOStatement 没有把绑定的参数和列报告给循环收集器,引用成环时回收会滞后。

mysqlnd 层

OK 包里的 field_count 没有重置,前一次结果集的字段数会带到后续操作。连接被杀之后再去关闭预处理语句会泄漏内存。另有 GHSA-r6x9-5r99-36j7(CVE-2025-1218):mysqlnd 线协议存在多处越界读,输入来源不可信时这是需要优先修的一类。

session 严格模式的失败路径

session.use_strict_mode 拒绝客户端提供的 ID 后要走 create_sid();如果它失败,session_start() 仍继续执行,后续就在一个无效会话上操作。用户自定义的 create_sid() 抛出的异常,会被返回值校验错误覆盖,原始原因丢失。mod_mm 中另有数据损坏问题,GH-23043 记录坏的 session id 代码可能导致 zend_mm_heap corrupted。

驱动侧的其他修复

PDO_DBLIB:PDO 事务内查询失败后 segfault;beginTransaction、commit、rollBack、lastInsertId 的错误现在会正常上报(GH-23741)。

PDO_PGSQL:PDO::CURSOR_SCROLL 在 PDO::ATTR_PREFETCH => 0 的懒取模式下失败。此前还修过清理 COPY 中遗留 fetch 时的死循环、销毁使用模拟或关闭 prepare 的语句时的 use-after-free、连接被留给下一次 fetch 时仍处于 busy、以及行数据来自被另一个语句接管的结果集。

MySQLi:重连失败后访问 mysqli 属性触发断言失败(GH-22854)。

排查顺序

  1. 先看事务:复用连接前调 inTransaction()。返回 true 就不要继续,先回滚或提交。应用层自己记的事务标志与驱动状态不一致时,以驱动为准。
  2. 关游标:closeCursor() 显式结束上一次的结果集,别依赖下一次 execute() 隐式覆盖,尤其是执行可能失败的分支。
  3. rowCount 只用于写操作:rowCount() 对 SELECT 不可靠,只能用于 INSERT/UPDATE/DELETE 的受影响行数判断。
  4. 复用即清理:显式重置 fetch mode、绑定参数、绑定列和语句属性;不要在持久连接上假设“上一个请求已经收拾干净”。

短命连接上这些问题大多被连接销毁掩盖了,换成持久连接就变成跨请求的数据正确性问题。

对应修复版本

上述大部分条目在 PHP 8.5.12(2026-10-22)发布,部分来自 8.5.11(2026-09-24)。升级后仍建议保留上面的显式清理逻辑——修复的是引擎侧的越界、泄漏和生命周期问题,不改变“持久连接共享状态”这个前提。

复制全文 生成海报 PHP MySQL PDO 数据库

推荐文章

程序员茄子在线接单