因为PDO实例不感知读写语义,连接固定指向单一数据库,无法自动路由SQL;需封装连接获取逻辑,由调用方显式声明read/write意图,并维护主从连接池,禁用从库模拟预处理以避免错误。

为什么直接用 $pdo = new PDO(...) 无法实现读写分离
因为 PDO 实例本身不感知“读”或“写”语义,PDO 连接一旦建立,就固定指向某个数据库地址。执行 SELECT 或 INSERT 全靠开发者手动选连接——没人拦你往从库发 UPDATE,也没人帮你把 SELECT 自动路由到从库。
真正要做的,是把“获取连接”这个动作封装起来,根据当前 SQL 类型(或显式标记)返回主库或从库的 PDO 实例。
- 不能靠 SQL 解析判断读写(
SELECT COUNT(*) FROM ...是读,但SELECT ... FOR UPDATE是写) - 更可靠的方式:由调用方显式声明意图,比如
db()->read()->query(...)或db()->write()->prepare(...) - 主库连接必须支持事务、写操作、自增 ID;从库连接应设
PDO::ATTR_EMULATE_PREPARES => false避免预处理语句在只读库上失败
如何设计一个轻量级读写分离调度器类
核心就三件事:维护主从连接池、提供读/写连接获取接口、避免重复创建连接。不需要抽象出“策略模式”或“负载均衡算法”,简单轮询从库即可。
示例类结构:
class DbRouter {
private static $master;
private static $slaves = [];
private static $slaveIndex = 0;
public static function master(): PDO {
if (!self::$master) {
self::$master = new PDO('mysql:host=master-host;dbname=test', $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => true,
]);
}
return self::$master;
}
public static function slave(): PDO {
$slaves = ['slave1-host', 'slave2-host'];
$host = $slaves[self::$slaveIndex % count($slaves)];
self::$slaveIndex++;
$key = $host;
if (!isset(self::$slaves[$key])) {
self::$slaves[$key] = new PDO("mysql:host={$host};dbname=test", $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_PERSISTENT => true,
// 关键:禁用模拟预处理,防止从库报错
PDO::ATTR_EMULATE_PREPARES => false,
]);
}
return self::$slaves[$key];
}
}
- 用
static缓存连接,避免每次请求都新建 PDO(否则连接池失效) - 从库连接加
PDO::ATTR_EMULATE_PREPARES => false,否则PREPARE在只读库会抛SQLSTATE[HY000]: General error: 1290 The MySQL server is running with the --read-only option - 不要在
__destruct或register_shutdown_function中 close 连接——PHP-FPM 模式下连接会被复用,主动 close 反而增加开销
怎么在实际查询中正确调用读/写连接
最易踩坑的是“隐式写操作”:比如 INSERT ... ON DUPLICATE KEY UPDATE 看似带 INSERT,实则是写;SELECT ... LOCK IN SHARE MODE 也是写(加锁)。这些必须走主库。
推荐做法:默认走主库,仅对明确无副作用的 SELECT 显式调用 slave()。
- 写操作统一走
DbRouter::master():INSERT、UPDATE、DELETE、REPLACE、含FOR UPDATE/LOCK IN SHARE MODE的SELECT - 读操作可走
DbRouter::slave():SELECT(不含锁、不含变量赋值、不含子查询写操作) - 事务内所有操作必须用同一个连接,且只能是主库:
DbRouter::master()->beginTransaction()后,后续query必须也用该实例 - 避免在从库执行
SELECT LAST_INSERT_ID()或SELECT @@insert_id——从库可能延迟,返回旧值
主从延迟导致读不到刚写入的数据怎么办
这不是调度器能解决的问题,而是架构约束。常见应对方式只有两种:业务容忍、或强制走主库。
- 用户注册后立即查自己资料 → 必须用
DbRouter::master()->query(...),别贪那点从库性能 - 订单支付后查订单状态 → 同样走主库,或加缓存层兜底(查缓存未命中再查主库)
- 不要试图在调度器里“等从库同步完成”(比如轮询
SHOW SLAVE STATUS),延迟不可控且阻塞请求 - 如果大量场景需要强一致性读,说明从库不适合承担这部分流量,不如先优化主库或加缓存
读写分离不是银弹。连接调度器只是把“选库”这件事收口了,但什么时候该读主库、哪些数据不能接受延迟——得靠业务逻辑自己判断,这点最容易被忽略。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











