☰
PHP的PDO连接讲解
2026/10/9 17:10:06 网站建设 项目流程

前言


PDO 的连接方式有个很好的设计:不管你连的是哪种数据库,用的都是同一个类名PDO,差异全部收进第一个参数——DSN(Data Source Name,数据源名称)。手册的说法是"创建 PDO 基类的实例即可建立连接,无论想用哪个驱动,类名总是 PDO"。


这种统一带来了三个高频误解。


误解一:以为连接会一直有效。PDO 实例的生命周期就是它所在变量的生命周期。变量被回收,连接就断。手册专门提醒:要主动关闭连接,得把持有它的变量赋成null;而且如果还有PDOStatement实例或别的变量引用着同一个 PDO 对象,这些引用也得一并清掉,否则连接不会关闭。


误解二:以为持久连接是"免费的加速"。手册对它的警告相当重:PDO 不对持久连接做任何清理,上一次使用留下的临时表、锁、事务等状态可能还在。


误解三:以为构造失败了会返回false。手册在连接章节写得很干脆——连接出错时会抛PDOException,而且这一点不受PDO::ATTR_ERRMODE设置影响。


本文按 PHP 8.1+ 讲清 DSN 的拼装、连接的生命周期管理、持久连接的真实代价,以及几个构造函数选项该怎么选。


一、DSN 的结构


DSN 的通用形状是"驱动名冒号,随后一串以分号分隔的键值对":


驱动名:键1=值1;键2=值2

它作为第一个参数传给构造函数:


<?php // 适用于 PHP 8.0+
public PDO::__construct(
string $dsn,
?string $username = null,
?string $password = null,
?array $options = null
)

各驱动的常用键位不同,下面这张表是常见的形态:




驱动DSN 示例说明



MySQLmysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4支持unix_socket走本地套接字

PostgreSQLpgsql:host=127.0.0.1;port=5432;dbname=shop也支持sslmode等键

SQLitesqlite:/data/shop.sq3直接指向文件路径

SQLite 内存库sqlite::memory:库只存在于当前连接

SQL Serversqlsrv:Server=127.0.0.1,1433;Database=shop注意端口用逗号连接

Oracleoci:dbname=//127.0.0.1:1521/shop;charset=AL32UTF8字符集键名与其他驱动不同



sqlite::memory:值得留意:因为库活在连接里,连接一断数据就没了。写单元测试时很合适,误当成生产库用就是灾难。


MySQL 上一定要写charset=utf8mb4。不写的话驱动会用编译期默认字符集,很多环境上还是latin1或utf8(三字节版),存四字节字符(emoji、部分生僻汉字)时会静默截断或报错。写进 DSN 比事后执行SET NAMES更可靠,因为它作用在连接建立阶段。


二、连接的生命周期与关闭


<?php // 适用于 PHP 8.0+
$pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

$stmt = $pdo->query('SELECT id FROM users LIMIT 10');

// 想立刻关闭连接:两条引用都要断
$stmt = null;
$pdo = null;

手册对这个例子的说明是:连接在 PDO 对象的生命周期内保持活动,要关闭它必须销毁对象,办法是把持有它的变量赋值为null;如果还有别的引用指向同一个 PDO 实例(比如某个PDOStatement),那些引用也必须一起移除,否则连接不会真的关闭。


这条规则在两类场景里很重要:



  • 批量脚本:一个长驻进程里反复建连接,如果不显式释放,连接数会持续累积。

  • 持久连接之外的长脚本:虽然 PHP 脚本结束时连接会自动关闭,但脚本中途需要重建连接的场合(切换数据库、切换租户),必须手动断开。


另一条与连接相关的安全提示来自手册:未捕获的PDOException会产生致命错误,致命错误的回溯里可能带着连接细节。所以生产服务器的display_errors应当设为0,错误只写日志。


正确的连接封装长这样:


<?php // 适用于 PHP 8.0+
declare(strict_types=1);

function makePdo(array $cfg): PDO
{
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
$cfg['host'],
$cfg['port'],
$cfg['dbname']
);

try {
return new PDO($dsn, $cfg['user'], $cfg['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_TIMEOUT => 5,
]);
} catch (PDOException $e) {
// 原始消息只进日志;对外给统一提示
error_log('DB connect failed: ' . $e->getMessage());
throw new RuntimeException('数据库连接失败', 0, $e);
}
}

PDO::ATTR_TIMEOUT的作用是"设置与数据库通信的超时秒数",它管的是 PDO 与数据库之间的通信等待,不是 HTTP 请求超时,也不是 TCP 连接超时(后者靠 MySQL 的connect_timeout)。这几个超时概念层次不同,排查"连接慢"时容易混淆。


三、持久连接:省下的是什么,付出的是什么


持久连接的动机很直接:脚本结束时连接不关闭而是被缓存起来,下一个使用相同凭据的脚本请求可以复用,省掉每次重新建立连接的开销。手册说这能让 Web 应用更快。


用法只有一种正确姿势——在构造函数里设:


<?php // 适用于 PHP 8.0+,仅在构造函数中设置才有效
$pdo = new PDO('mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4', $user, $pass, [
PDO::ATTR_PERSISTENT => true,
]);

手册对这里有三条明确的约束,一条比一条容易踩:


第一条:只能用构造函数设。手册写着,如果实例化之后再用PDO::setAttribute()设置这个属性,驱动不会使用持久连接。也就是说"先建好了再补一个 setAttribute"是无效操作,而且不会报错,只是安静地不生效。


第二条:值会被转成bool,除非它是非数字字符串。手册原文的意思是:这个值会被转换成布尔值(开启或关闭持久连接);但如果传的是一个非数字字符串,就可以用来区分多个持久连接池。这个冷门特性在"不同连接需要不兼容设置"时有用,手册举的例子是PDO::MYSQL_ATTR_USE_BUFFERED_QUERY取值不同。


第三条,也是最重要的一条:PDO 不做任何清理。



PDO does not perform any cleanup of persistent connections. Temporary tables, locks, transactions and other stateful changes may remain from previous usage of the connection, causing unexpected problems.



翻译过来:上次用完留下的临时表、行锁、未结束的事务,都可能还挂在连接上,导致下一次使用时出现莫名其妙的问题。典型症状包括:



  • 上一个请求开的事务没提交,下一个请求在同一个连接上直接执行语句,行为变得难以预测;

  • 临时表重名冲突;

  • 会话变量(包括前面说的隔离级别、SQL_MODE)被上一个请求改过,在你毫无察觉时影响着当前请求。


因此如果要用持久连接,必须做到:每个请求结束时把状态归零——事务必须提交或回滚,临时表显式删除,会话级设置要么在连接时可以重置要么在请求入口重新设置一次。


手册还提到一个例外情形:如果你用的是 PDO 的 ODBC 驱动,并且底层 ODBC 库自带连接池(unixODBC、Windows 都算),那么建议不要用 PDO 的持久连接,把缓存交给 ODBC 连接池那一层。原因是 ODBC 连接池是进程内多模块共享的;PDO 一旦把连接缓存住,它就永远不会归还给 ODBC 池,反而会为其他模块新建额外连接。


顺带提一下连接池相关的另一个属性PDO::ATTR_PREFETCH:手册说明它在多数驱动上用于设置预取行数,而PDO_PGSQL驱动把它当成开关来用。这类"同名属性在不同驱动上语义不同"的情况,在 PDO 里并不罕见,遇到时要查对应驱动页面而不是只看核心手册。


常见坑点



  1. DSN 里漏了字符集


❌mysql:host=127.0.0.1;dbname=shop✅mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4,避免四字节字符出问题



  1. 以为unset()就能关连接


❌unset($pdo);然后指望连接立刻关闭 ✅ 把变量和所有PDOStatement引用一并置null,手册明确要求清掉全部引用



  1. 持久连接用setAttribute()设置


❌$pdo = new PDO(...); $pdo->setAttribute(PDO::ATTR_PERSISTENT, true);✅ 只能写在构造函数的选项数组里,事后设置不生效且不报错



  1. 用了持久连接却不清理事务状态


❌ 请求异常退出留下未提交事务,下一个请求复用同一连接 ✅ 每个请求出口保证inTransaction()为假,或干脆不用持久连接



  1. 把连接异常当返回值处理


❌$pdo = new PDO(...); if ($pdo === false) { ... }✅ 构造函数失败总是抛PDOException,必须try/catch



  1. 生产环境开着display_errors


❌ 未捕获的连接异常把回溯打到页面上 ✅display_errors=0,手册提示回溯可能泄露连接细节



  1. 把PDO::ATTR_TIMEOUT当成 HTTP 超时


❌ 用它来控制接口整体响应时间 ✅ 它只管与数据库的通信超时;TCP 连接超时看驱动的连接参数,HTTP 超时看 cURL 或 Web 服务器配置



  1. 用sqlite::memory:跑生产


❌ 以为内存库配持久连接就能留存数据 ✅ 内存库随连接消亡,持久化场景用文件路径


总结




项目说明



构造函数PDO::__construct(string $dsn, ?string $username, ?string $password, ?array $options)

失败行为总是抛PDOException,与PDO::ATTR_ERRMODE无关

DSN 形态驱动名:键=值;键=值,各驱动键位不同

关闭连接所有引用(含PDOStatement)都要置null

持久连接PDO::ATTR_PERSISTENT,只能在构造函数里设置

持久连接代价PDO 不做任何清理,事务、锁、临时表、会话变量都可能残留

通信超时PDO::ATTR_TIMEOUT只管与数据库的通信



把连接当成一个有明确生命周期的对象来管理,是这一章的核心。DSN 拼装时别忘字符集,关闭连接时别忘清引用,启用持久连接前先确认自己有能力把连接状态归零。至于构造失败一定抛异常这一条,记住它就能避免写出永远不会触发的if ($pdo === false)判断。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询