PHP读写分离实现指南:架构设计、代码实践与高频问题解析
目录导读
- 什么是读写分离?为什么要做?
- 核心实现方案:从DNS到代码层的四种路径
- 基于PDO和MySQLi的手动切换实现(附完整代码)
- 基于框架(Laravel/ThinkPHP)的读写分离配置
- 高可用进阶:多从库负载均衡与故障转移
- 常见问题与面试问答(FAQ)

什么是读写分离?为什么要做?
读写分离(Read-Write Splitting)是将数据库的读操作(SELECT)与写操作(INSERT/UPDATE/DELETE)分发到不同的数据库实例上,主库(Master)负责写入,从库(Slave)负责读取。
核心收益:
- 减轻主库的CPU/IO压力,提升写入性能
- 增加查询并发能力,尤其适合读多写少场景(如电商、内容站)
- 配合主从复制(Replication)实现数据冗余
⚠️ 前置条件:必须配置好MySQL主从复制(Binlog + Relay Log),确保从库数据最终一致性。
核心实现方案:从DNS到代码层的四种路径
| 方案层级 | 技术手段 | 优点 | 缺点 |
|---|---|---|---|
| DNS/负载均衡层 | LVS、HAProxy、MySQL Proxy | 对应用透明 | 网络开销大,SQL解析复杂 |
| 数据库中间件 | MyCat、ShardingSphere、ProxySQL | 功能丰富(分片+读写分离) | 引入额外组件,运维复杂 |
| PHP扩展层 | mysqlnd_ms(MySQL Native Driver) | 低延迟,无需改代码 | 配置复杂,仅限PHP环境 |
| 代码层(本文重点) | PDO/MySQLi,或框架配置 | 灵活可控,易调试 | 需侵入业务代码 |
推荐:中小项目优先代码层,大型项目考虑ProxySQL或ShardingSphere。
基于PDO和MySQLi的手动切换实现(附完整代码)
1 方式一:PDO + 自定义连接类
class DbManager {
private $master;
private $slave;
public function __construct() {
// 主库配置
$this->master = new PDO(
'mysql:host=master_host;dbname=test',
'root', 'pass',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// 从库配置(可传入数组实现多从库)
$this->slave = new PDO(
'mysql:host=slave_host;dbname=test',
'root', 'pass',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
/**
* 核心方法:根据SQL前缀自动选择连接
*/
public function query($sql, $params = []) {
$isRead = stripos(trim($sql), 'SELECT') === 0;
// 事务必须用主库(防止复制延迟导致数据不一致)
if ($this->inTransaction) {
$conn = $this->master;
} else {
$conn = $isRead ? $this->selectSlave() : $this->master;
}
$stmt = $conn->prepare($sql);
$stmt->execute($params);
return $stmt;
}
private function selectSlave() {
// 简单轮询或随机算法
return $this->slave;
}
public function beginTransaction() {
$this->inTransaction = true;
return $this->master->beginTransaction();
}
// ... commit / rollback 同理
}
2 方式二:MySQLi + 封装函数
function db_detect($sql) {
$sql = ltrim($sql);
if (preg_match('/^(SELECT|SHOW|DESC|EXPLAIN)/i', $sql)) {
return 'slave';
}
return 'master';
}
// 使用时调用不同的mysqli连接
$conn = (db_detect($sql) === 'slave') ? $slaveConn : $masterConn;
关键点:
- 必须处理
SELECT ... FOR UPDATE(需要主库,因为涉及行锁) - 立即需要看到刚写入的数据(主库写后同请求内读),需强制走主库
基于框架(Laravel/ThinkPHP)的读写分离配置
1 Laravel(使用DB facade或Eloquent)
在.env或config/database.php中:
'mysql' => [
'read' => [
['host' => '192.168.1.101'],
['host' => '192.168.1.102'], // 多个从库自动轮询
],
'write' => [
['host' => '192.168.1.100'],
],
'sticky' => true, // 写后立即读走主库
'driver' => 'mysql',
'database' => 'app_db',
'username' => 'root',
'password' => 'secret',
],
Laravel自动识别SELECT走read,其他走write,若设置sticky=true,同请求内写入后读取自动用主库。
2 ThinkPHP 6.x
在config/database.php中:
'connections' => [
'mysql' => [
'type' => 'mysql',
'host' => 'master_host',
// ...
'read' => [
'host' => 'slave_host1,slave_host2',
],
'write' => [
'host' => 'master_host',
],
],
],
ThinkPHP同样内置SQL语句智能判断。
高可用进阶:多从库负载均衡与故障转移
1 负载均衡策略
- 轮询(Round Robin):简单按顺序分配
- 权重轮询:根据服务器性能配置权重
- 随机:适合同配置机器
2 故障转移(Failover)
public function selectSlave() {
foreach ($this->slaves as $slave) {
try {
$conn = $this->connect($slave);
$conn->query('SELECT 1'); // 健康检查
return $conn;
} catch (Exception $e) {
// 记录错误,尝试下一个
continue;
}
}
// 全部失败,回退主库(降级处理)
return $this->master;
}
3 复制延迟处理
- 方案一:根据主库
SHOW MASTER STATUS与从库SHOW SLAVE STATUS计算延迟秒数,超过阈值(如5秒)时新写入的数据强制走主库。 - 方案二:基于缓存标记(Redis),写操作设key,读操作先查key是否存在,存在则走主库。
常见问题与面试问答(FAQ)
Q1: 读写分离后,为什么读到的数据是旧的?
答:因为MySQL主从复制是异步的,从库数据存在毫秒级延迟,解决方法:① 业务允许短暂不一致,则忽略;② 对强一致性要求高的数据,在代码中强制使用主库(Session、支付订单等);③ 使用sticky机制或半同步复制。
Q2: SELECT COUNT(*) 应该走从库吗?
答:可以,但要注意统计结果的精确性,如果应用逻辑需要在同一事务内多次读取相同数据,且期间有写入,那必须走主库。
Q3: 一个请求中既有写又有读,如何设计?
答:最佳实践是默认读走从库,但当前请求执行了写操作后,标记该请求的后续读走主库(例如Laravel的sticky=true),或者在写操作后,使用Redis记录用户ID,设置5秒内该用户强制读主库。
Q4: 如果从库宕机怎么办?
答:① 代码层实现自动故障转移,轮询前先SELECT 1检测,失败则跳过;② 全部从库挂了则降级到主库,保障业务可用性;③ 建议配置监控(如Prometheus + alert)及时报警。
Q5: 如何监控读写分离的流量分配?
答:可以在代码连接层埋点,记录SQL执行日志(含来源库标识),也可以从MySQL侧查看SHOW GLOBAL STATUS LIKE 'Com_select'和Com_insert在不同实例上的比例。
Q6: 有哪些开源的PHP读写分离组件?
答:① php-mysql-replication(解析binlog,间接实现);② mysqlnd_ms(官方扩展,已不再维护但稳定);③ 中间件MyCat、ProxySQL等。
PHP读写分离的核心是分流策略的正则判断和连接管理,建议中小企业直接用框架配置(如Laravel),大型项目采用ProxySQL,始终记住:事务、锁、强一致性读必须走主库,同时监控复制延迟是运维底线。
最后提示:本文所有示例代码可基于本地Docker搭建主从环境进行实战验证,实践出真知,动手操练一遍方能深刻掌握。