PHP项目如何实现读写分离?

wen java案例 1

本文目录导读:

PHP项目如何实现读写分离?

  1. 方案一:利用数据库中间件(推荐企业级)
  2. 方案二:使用 PHP ORM(面向现代框架)
  3. 方案三:手动实现 DBAL(数据库抽象层)
  4. 方案四:域名解析(简单粗暴)
  5. 必须注意的核心问题
  6. 总结建议

在 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() 标记。
    • 缓存:用户写入后,将数据同步到 Redis,从库未同步前先从缓存读。
    • 按需延迟:对一致性要求不高的查询(如历史日志)可以读从库。

事务处理

  • 问题:如果在一个事务内先 UPDATE(主库)再 SELECT(从库),从库可能查不到刚刚更新的数据。
  • 方案:一旦开启事务,该连接的所有操作必须强制使用主库。

连接数及性能

  • 方案一(中间件) 通常支持连接池,对系统资源更友好。
  • 方案二(ORM) 需要关注框架自带的连接池实现,避免频繁建立/断开连接。

总结建议

项目类型 推荐方案
新建大型项目 / 高并发项目 ProxySQL 或 MaxScale(中间件) + 框架自带支持,实现解耦,方便运维。
已有框架项目(Laravel/ThinkPHP) 配置框架的读写分离,并处理好 强制读主 以应对延迟问题。
老旧原生项目 / 微服务 手动封装 DBAL集成一个轻量级 DBAL 库,控制路由逻辑。
测试环境 / 临时方案 可以使用 MySQL Proxy(已较旧)或简单的域名解析。

一个非常实用的建议: 如果项目团队没有专门的 DBA(数据库管理员)或运维人员,并且对高并发要求不高,不要轻易引入读写分离,因为主从延迟和故障转移带来的问题,有时候比单库的性能瓶颈更让人头疼,可以先尝试使用 Redis 缓存读 + 数据库优化 来解决性能问题,如果确实需要分离,建议优先考虑 中间件方案(ProxySQL),让运维负责中间件,开发人员专注业务逻辑。

抱歉,评论功能暂时关闭!