PHP数据库读写分离是提升高并发场景下数据库性能的核心方案,核心思路是将写操作指向主数据库,读操作分发到多个从数据库,结合主从复制保证数据一致性,再通过负载均衡优化从库的请求分配。
一、MySQL主从复制配置
主从复制是读写分离的基础,需要先完成主从数据库的环境搭建与同步配置,这里以MySQL 8.0为例说明步骤。
1. 主数据库配置
首先修改主库的配置文件,Linux系统下路径通常为/etc/my.cnf,Windows系统下为my.ini,添加以下配置:
[mysqld] # 启用二进制日志,主从复制必须开启 log-bin=mysql-bin # 设置服务器唯一ID,主从库ID不能重复 server-id=1 # 需要同步的数据库名,多个数据库可重复配置 binlog-do-db=test_db # 忽略同步的数据库 binlog-ignore-db=mysql
配置完成后重启MySQL服务,然后登录主库执行以下命令创建用于同步的账号并授权:
-- 创建同步账号,密码为slave_pass CREATE USER 'slave_user'@'%' IDENTIFIED BY 'slave_pass'; -- 授予同步权限 GRANT REPLICATION SLAVE ON *.* TO 'slave_user'@'%'; -- 刷新权限 FLUSH PRIVILEGES; -- 查看主库状态,记录File和Position值 SHOW MASTER STATUS;
2. 从数据库配置
修改从库的配置文件,设置唯一的服务器ID:
[mysqld] # 从库唯一ID,不能和主库及其他从库重复 server-id=2 # 启用中继日志 relay-log=mysql-relay
重启从库服务后,登录从库执行同步配置命令,将之前记录的主库File和Position值填入:
-- 配置主库连接信息 CHANGE MASTER TO MASTER_HOST='主库IP地址', MASTER_USER='slave_user', MASTER_PASSWORD='slave_pass', MASTER_LOG_FILE='mysql-bin.000001', -- 替换为SHOW MASTER STATUS得到的File值 MASTER_LOG_POS=156; -- 替换为SHOW MASTER STATUS得到的Position值 -- 启动同步 START SLAVE; -- 查看同步状态,确保Slave_IO_Running和Slave_SQL_Running都是Yes SHOW SLAVE STATUSG
二、PHP层读写分离实现
完成主从复制后,需要在PHP代码中实现读写请求的分发逻辑,这里以PDO扩展为例实现简单的读写分离。
1. 基础配置类实现
首先定义数据库配置数组,区分主库和从库信息:
<?php
class DbConfig {
// 主库配置,处理写操作
public static $master = [
'host' => '127.0.0.1',
'port' => 3306,
'dbname' => 'test_db',
'user' => 'root',
'pass' => 'master_pass'
];
// 从库配置数组,处理读操作,可配置多个从库
public static $slaves = [
[
'host' => '192.168.0.2',
'port' => 3306,
'dbname' => 'test_db',
'user' => 'root',
'pass' => 'slave_pass1'
],
[
'host' => '192.168.0.3',
'port' => 3306,
'dbname' => 'test_db',
'user' => 'root',
'pass' => 'slave_pass2'
]
];
}
2. 读写分离连接类实现
实现连接管理类,根据SQL语句类型选择对应的数据库连接:
<?php
class DbConnection {
private $masterPdo;
private $slavePdos = [];
// 获取主库连接
public function getMasterPdo() {
if (!$this->masterPdo) {
$cfg = DbConfig::$master;
$dsn = "mysql:host={$cfg['host']};port={$cfg['port']};dbname={$cfg['dbname']};charset=utf8mb4";
$this->masterPdo = new PDO($dsn, $cfg['user'], $cfg['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
return $this->masterPdo;
}
// 随机获取一个从库连接,实现简单负载均衡
public function getSlavePdo() {
$slaveCfg = DbConfig::$slaves[array_rand(DbConfig::$slaves)];
$key = $slaveCfg['host'] . ':' . $slaveCfg['port'];
if (!isset($this->slavePdos[$key])) {
$dsn = "mysql:host={$slaveCfg['host']};port={$slaveCfg['port']};dbname={$slaveCfg['dbname']};charset=utf8mb4";
$this->slavePdos[$key] = new PDO($dsn, $slaveCfg['user'], $slaveCfg['pass'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]);
}
return $this->slavePdos[$key];
}
// 根据SQL语句判断使用主库还是从库
public function getPdoBySql($sql) {
$sql = trim($sql);
// 写操作相关关键字,匹配则使用主库
$writeKeywords = ['INSERT', 'UPDATE', 'DELETE', 'REPLACE', 'CREATE', 'ALTER', 'DROP'];
foreach ($writeKeywords as $keyword) {
if (stripos($sql, $keyword) === 0) {
return $this->getMasterPdo();
}
}
// 读操作使用从库
return $this->getSlavePdo();
}
}
3. 使用示例
调用连接类执行数据库操作:
<?php
$db = new DbConnection();
// 写操作,自动使用主库
$writeSql = "INSERT INTO user (name, age) VALUES ('张三', 25)";
$pdo = $db->getPdoBySql($writeSql);
$pdo->exec($writeSql);
// 读操作,自动使用从库
$readSql = "SELECT * FROM user WHERE age > 20";
$pdo = $db->getPdoBySql($readSql);
$result = $pdo->query($readSql)->fetchAll();
print_r($result);
三、负载均衡优化设置
上述示例中使用随机算法分配从库请求,实际场景中可以根据需求调整负载均衡策略:
- 轮询策略:按顺序依次分配请求到每个从库,保证请求均匀分布
- 权重策略:根据从库的性能配置不同权重,性能高的从库分配更多请求
- 故障转移:检测从库连接状态,自动剔除不可用的从库,避免请求失败
以下是权重轮询策略的实现示例:
<?php
class WeightedRoundRobin {
private $servers = [];
private $currentIndex = 0;
private $totalWeight = 0;
public function __construct($servers) {
// $servers格式:[['host' => 'xxx', 'weight' => 2], ...]
$this->servers = $servers;
foreach ($servers as $server) {
$this->totalWeight += $server['weight'];
}
}
public function getServer() {
$server = $this->servers[$this->currentIndex % count($this->servers)];
$this->currentIndex++;
return $server;
}
}
// 配置带权重的从库
$slaves = [
['host' => '192.168.0.2', 'weight' => 2, 'port' => 3306, 'user' => 'root', 'pass' => 'slave_pass1'],
['host' => '192.168.0.3', 'weight' => 1, 'port' => 3306, 'user' => 'root', 'pass' => 'slave_pass2']
];
$robin = new WeightedRoundRobin($slaves);
// 获取从库时调用$robin->getServer()即可得到按权重分配的从库信息
四、注意事项
配置读写分离时需要注意以下问题:
- 主从复制存在延迟,写操作后立刻读可能读不到最新数据,需要根据业务场景做延迟处理或使用主库读
- 事务操作中的读写都需要使用主库连接,避免事务内数据不一致
- 定期监控主从同步状态,及时处理同步异常问题
- 从库数量需要根据实际读请求量调整,避免从库过载