PHP读写分离怎么实现

wen PHP项目 7

PHP读写分离实现指南:架构设计、代码实践与高频问题解析

目录导读

  1. 什么是读写分离?为什么要做?
  2. 核心实现方案:从DNS到代码层的四种路径
  3. 基于PDO和MySQLi的手动切换实现(附完整代码)
  4. 基于框架(Laravel/ThinkPHP)的读写分离配置
  5. 高可用进阶:多从库负载均衡与故障转移
  6. 常见问题与面试问答(FAQ)

PHP读写分离怎么实现

什么是读写分离?为什么要做?

读写分离(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)

.envconfig/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搭建主从环境进行实战验证,实践出真知,动手操练一遍方能深刻掌握。

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