
前言PDO 的连接方式有个很好的设计不管你连的是哪种数据库用的都是同一个类名PDO差异全部收进第一个参数——DSNData 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.0public PDO::__construct(string $dsn,?string $username null,?string $password null,?array $options null)各驱动的常用键位不同下面这张表是常见的形态驱动DSN 示例说明MySQLmysql:host127.0.0.1;port3306;dbnameshop;charsetutf8mb4支持unix_socket走本地套接字PostgreSQLpgsql:host127.0.0.1;port5432;dbnameshop也支持sslmode等键SQLitesqlite:/data/shop.sq3直接指向文件路径SQLite 内存库sqlite::memory:库只存在于当前连接SQL Serversqlsrv:Server127.0.0.1,1433;Databaseshop注意端口用逗号连接Oracleoci:dbname//127.0.0.1:1521/shop;charsetAL32UTF8字符集键名与其他驱动不同sqlite::memory:值得留意因为库活在连接里连接一断数据就没了。写单元测试时很合适误当成生产库用就是灾难。MySQL 上一定要写charsetutf8mb4。不写的话驱动会用编译期默认字符集很多环境上还是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.0declare(strict_types1);function makePdo(array $cfg): PDO{$dsn sprintf(mysql:host%s;port%d;dbname%s;charsetutf8mb4,$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:host127.0.0.1;dbnameshop;charsetutf8mb4, $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 里并不罕见遇到时要查对应驱动页面而不是只看核心手册。常见坑点DSN 里漏了字符集❌mysql:host127.0.0.1;dbnameshop✅mysql:host127.0.0.1;dbnameshop;charsetutf8mb4避免四字节字符出问题以为unset()就能关连接❌unset($pdo);然后指望连接立刻关闭 ✅ 把变量和所有PDOStatement引用一并置null手册明确要求清掉全部引用持久连接用setAttribute()设置❌$pdo new PDO(...); $pdo-setAttribute(PDO::ATTR_PERSISTENT, true);✅ 只能写在构造函数的选项数组里事后设置不生效且不报错用了持久连接却不清理事务状态❌ 请求异常退出留下未提交事务下一个请求复用同一连接 ✅ 每个请求出口保证inTransaction()为假或干脆不用持久连接把连接异常当返回值处理❌$pdo new PDO(...); if ($pdo false) { ... }✅ 构造函数失败总是抛PDOException必须try/catch生产环境开着display_errors❌ 未捕获的连接异常把回溯打到页面上 ✅display_errors0手册提示回溯可能泄露连接细节把PDO::ATTR_TIMEOUT当成 HTTP 超时❌ 用它来控制接口整体响应时间 ✅ 它只管与数据库的通信超时TCP 连接超时看驱动的连接参数HTTP 超时看 cURL 或 Web 服务器配置用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)判断。