本文目录导读:

在 PHP 项目中实现读写分离,核心思路是将读操作(SELECT)发送到从库,将写操作(INSERT、UPDATE、DELETE)发送到主库。
根据项目的复杂度和使用的技术栈,有以下几种主流实现方案:
利用数据库中间件(推荐企业级)
这是最优雅、对代码侵入性最低的方案,PHP 应用只需要连接一个“假数据库”(即中间件),中间件负责解析 SQL 语句并自动路由到主库或从库。
- 代表产品:
- ProxySQL:功能强大,支持读写分离、连接池、查询缓存、故障自动切换,性能很高。
- MaxScale:MariaDB 官方出品,与 MySQL/MariaDB 兼容性好。
- MyCat / ShardingSphere-Proxy:更偏向分库分表,但也支持读写分离。
- 优点:PHP 代码不需要任何改动,只需要把数据库连接地址改成中间件的 IP 和端口,运维层面配置即可。
- 缺点:需要额外部署和维护中间件服务,增加了一层网络开销(通常很小)。
// 代码完全不需要改动
$pdo = new PDO('mysql:host=中间件地址;port=6033;dbname=test', 'user', 'pass');
$pdo->query('UPDATE users SET name="new" WHERE id=1'); // 自动路由到主库
$pdo->query('SELECT * FROM users WHERE id=1'); // 自动路由到从库
使用 PHP ORM(面向现代框架)
如果项目使用了 Laravel、ThinkPHP、Symfony 等现代框架,其内置的 ORM(对象关系映射)通常提供了原生读写分离支持。
Laravel(Eloquent)
在 config/database.php 中配置:
'mysql' => [
'read' => [ // 读库配置(可以配多个,随机或轮询)
'host' => ['192.168.1.2', '192.168.1.3'], // 从库
],
'write' => [ // 写库配置
'host' => ['192.168.1.1'], // 主库
],
'driver' => 'mysql',
'database' => 'your_db',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
// ... 其他配置
]
- 工作原理:Eloquent 在执行
select时会自动连接从库,执行insert/update/delete时连接主库。 - 注意:如果使用
DB::transaction(),整个事务内的所有操作默认都会落到主库上,以保证事务一致性。
ThinkPHP 6+
在 config/database.php 中配置:
'connections' => [
'mysql' => [
'type' => 'mysql',
'hostname' => '主库IP', // 默认主库
// ... 其他参数
// 开启读写分离
'rw_separate' => true,
'read' => [
'hostname' => ['从库1IP', '从库2IP'],
],
'write' => [
'hostname' => ['主库IP'],
],
],
]
- 工作原理:ThinkPHP 的查询构造器会自动判断 SQL 类型进行路由。
手动实现 DBAL(数据库抽象层)
如果项目是原生 PHP 或使用了简单的 DBAL,可以自己封装一个数据库连接类。
class DB {
private static $master = null; // 主库连接
private static $slaves = []; // 从库连接池
public static function getConnection($sql) {
$command = strtoupper(trim(substr($sql, 0, 6))); // 取前6个字符判断
if (in_array($command, ['SELECT', 'SHOW', 'DESC', 'EXPLAIN'])) {
// 读操作:随机选择一个从库
$slaveConfig = self::getRandomSlaveConfig();
return self::$slaves[$slaveConfig['host']] ?? self::connect('slave', $slaveConfig);
} else {
// 写操作:连接主库
return self::$master ?? self::connect('master');
}
}
// 使用示例
public static function query($sql) {
$conn = self::getConnection($sql);
return $conn->query($sql);
}
}
- 优点:完全可控,适合简单项目或对 SQL 路由有特殊要求的场景。
- 缺点:需要自己处理连接池、故障转移、主从延迟等问题。
域名解析(简单粗暴)
- 方式:在 DNS 层面做文章,写操作走
write.yourdb.com(解析到主库),读操作走read.yourdb.com(解析到从库或从库负载均衡器)。 - 代码:在代码中区分使用不同的连接。
- 适用场景:极其简单或临时过渡,不推荐用于生产环境,因为无法应对 IP 变化、故障转移困难。
必须注意的核心问题
无论采用哪种方案,读写分离后都会面临以下挑战,需要提前设计好应对策略:
主从延迟(最麻烦)
- 问题:用户刚注册完(写主库),立刻刷新页面查自己的信息(读从库),如果数据还未同步到从库,用户会看到“用户不存在”。
- 解决方案:
- 强制读主:对于敏感操作后的第一次查询,强制落到主库。
- Laravel:使用
DB::connection('write')或->useWritePdo()。 - 手动:给封装类增加一个
forceMaster()标记。
- Laravel:使用
- 缓存:用户写入后,将数据同步到 Redis,从库未同步前先从缓存读。
- 按需延迟:对一致性要求不高的查询(如历史日志)可以读从库。
- 强制读主:对于敏感操作后的第一次查询,强制落到主库。
事务处理
- 问题:如果在一个事务内先
UPDATE(主库)再SELECT(从库),从库可能查不到刚刚更新的数据。 - 方案:一旦开启事务,该连接的所有操作必须强制使用主库。
连接数及性能
- 方案一(中间件) 通常支持连接池,对系统资源更友好。
- 方案二(ORM) 需要关注框架自带的连接池实现,避免频繁建立/断开连接。
总结建议
| 项目类型 | 推荐方案 |
|---|---|
| 新建大型项目 / 高并发项目 | ProxySQL 或 MaxScale(中间件) + 框架自带支持,实现解耦,方便运维。 |
| 已有框架项目(Laravel/ThinkPHP) | 配置框架的读写分离,并处理好 强制读主 以应对延迟问题。 |
| 老旧原生项目 / 微服务 | 手动封装 DBAL 或 集成一个轻量级 DBAL 库,控制路由逻辑。 |
| 测试环境 / 临时方案 | 可以使用 MySQL Proxy(已较旧)或简单的域名解析。 |
一个非常实用的建议: 如果项目团队没有专门的 DBA(数据库管理员)或运维人员,并且对高并发要求不高,不要轻易引入读写分离,因为主从延迟和故障转移带来的问题,有时候比单库的性能瓶颈更让人头疼,可以先尝试使用 Redis 缓存读 + 数据库优化 来解决性能问题,如果确实需要分离,建议优先考虑 中间件方案(ProxySQL),让运维负责中间件,开发人员专注业务逻辑。