本文目录导读:

在PHP项目中进行分片(Sharding)重分布并迁移数据至新节点,是一个涉及数据一致性、业务中断最小化的复杂操作,以下是通用的迁移方案和步骤,结合PHP项目的常见场景(如MySQL分库分表、Redis Cluster、MongoDB分片等)。
核心原则
- 灰度切换:逐步迁移流量,避免全量切换风险。
- 数据一致性:确保迁移过程中旧节点和新节点数据最终一致。
- 最小化停机:多数场景可采用在线迁移(双写 + 增量同步 + 灰度切换)。
- 回滚预案:每次迁移操作前准备回滚脚本。
常见分片架构及迁移策略
1 数据库分片(如 MySQL + ShardingSphere/MyCat)
场景:逻辑库拆分到多个物理库,需要新增节点并重新分配数据。
步骤:
- 新节点准备:搭建新数据库实例(主从架构),配置同步结构(表结构、索引)。
- 全量数据迁移:使用工具(如mysqldump、DTS)将旧分片中目标范围的数据导出并导入到新节点。
# 示例:从分片A(10.0.0.1:3306)迁移user_id=1~10000到新节点B(10.0.0.2:3306) mysqldump -h10.0.0.1 -uadmin -p --where="user_id BETWEEN 1 AND 10000" mydb users | mysql -h10.0.0.2 -uadmin -p mydb
- 增量同步:开启数据库级同步(如MySQL binlog解析、Canal/DTS)或应用层双写。
- 推荐方案:使用Canal监听旧节点binlog,解析后将增量数据写入新节点队列,由PHP消费写入新节点。
- 应用层双写:在DAO层增加条件判断,部分读写切换到新节点。
- 数据校验:对比新旧节点数据总量、关键字段哈希值(如MD5(所有字段))。
- 灰度切换路由:修改分片路由规则(如基于
user_id % 新碎片数),逐步将流量从旧节点切到新节点。 - 清理旧节点:确认新节点稳定后,移除旧节点或降低副本保留。
PHP代码示例(双写逻辑):
class UserShardRouter {
public function getShardConnection($userId) {
// 旧路由规则(迁移过渡期)
if ($this->isMigrating($userId)) {
// 新节点
return $this->newDbConnection;
}
return $this->oldDbConnection;
}
public function writeUser($userId, $data) {
$old = $this->oldDbConnection;
$new = $this->newDbConnection;
// 双写:先写旧节点(保证旧数据完整),再异步写新节点
$old->insert($data);
// 投递异步任务(如Redis队列)
$this->queue->push(['user_id' => $userId, 'data' => $data, 'target' => 'new']);
// 或者同步写新节点(增加耗时,但一致性更强)
// $new->insert($data);
}
}
2 Redis 分片(如 Redis Cluster / Codis)
场景:Redis 数据需要通过hash slot或一致性哈希迁移到新节点。
方案(以Redis Cluster为例):
- 自动迁移:
CLUSTER SETSLOT <slot> MIGRATING <source_node>+CLUSTER SETSLOT <slot> IMPORTING <target_node>+CLUSTER SETSLOT <slot> NODE <target_node>。 - 手工迁移:使用
redis-trib.rb或redis-cli --cluster reshard命令分配槽位,数据自动迁移。 - PHP注意:迁移过程中,
MOVED重定向会自动处理,无需代码改动。
3 自定义分片(如按用户ID取模)
场景:应用层自己维护路由表,数据映射到不同Redis/DB实例。
迁移步骤:
- 扩充分片数:从N个分片扩展到N+1个。
- 计算新路由:新分片键需满足旧分片数据不迁移(如使用一致性哈希虚拟节点,或二次分片:先按userId%固定基数(如1000),再映射到物理节点)。
- 双写阶段:写操作同时写入新旧节点,读操作只读旧节点。
- 数据迁移:后台脚本扫描旧节点,判断数据归属新节点后复制并删除旧节点数据(或标记清理)。
- 切换读操作:确认所有关键数据迁移完成后,读切到新节点。
- 清理:删除旧节点数据。
迁移过程中的PHP项目注意事项
- 连接管理:确保PHP连接池、PDO/Redis连接支持动态路由(如分片map可刷新)。
- 事务处理:双写或跨节点读写在事务边界内保证原子性(如使用消息队列补偿)。
- 读写分离:迁移期间,读请求可能回到旧节点,写请求双写,需防止脏读。
- 监控与告警:迁移日志中记录每个请求的分片路由、耗时,设置数据异常告警。
- 回滚脚本:准备反转路由规则、清除新节点数据、恢复旧节点路由的脚本。
自动化工具(可选)
- 数据库:阿里云DTS、AWS DMS、Prometheus(自研迁移框架)。
- Redis:Redis-shake(唯品会开源)、Redis Cluster原生迁移。
- 通用:Apache ZooKeeper/etcd(配置中心,动态下发路由规则)。
典型迁移流程示例(以MySQL分库为例)
// 1. 修改配置文件,增加新节点
$shardConfig = [
'shards' => [
'old' => ['host' => '10.0.0.1', 'db' => 'shard1'],
'new' => ['host' => '10.0.0.2', 'db' => 'shard2'],
],
// 迁移影响范围:userId % 10 == 0 的用户迁移到新节点
'migration_filter' => function($userId) { return $userId % 10 == 0; }
];
// 2. DAO层实现双写
class UserDao {
public function save($user) {
$userId = $user['id'];
$oldDb = $this->getDb('old');
$newDb = $this->getDb('new');
if ($this->config->migration_filter($userId)) {
// 新节点主写,旧节点双写
$newDb->insert($user);
$asyncQueue->push(['action' => 'write_to_old', 'data' => $user]);
} else {
// 旧节点直接写入
$oldDb->insert($user);
}
}
}
// 3. 后台迁移进程
class DataMigrationWorker {
public function migrate($batchSize = 1000) {
$oldDb = $this->getDb('old');
$newDb = $this->getDb('new');
$offset = 0;
do {
$rows = $oldDb->select("SELECT * FROM users WHERE id % 10 = 0 LIMIT $offset, $batchSize");
foreach ($rows as $row) {
// 插入新节点(忽略主键冲突)
$newDb->insertIgnore($row);
// 删除旧节点(或标记逻辑删除)
$oldDb->delete("DELETE FROM users WHERE id = ?", [$row['id']]);
}
$offset += $batchSize;
} while (count($rows) == $batchSize);
}
}
关键总结
- 数据迁移本质:全量同步 + 增量实时同步 + 校验切换。
- PHP代码改动点:路由规则、DAO层、连接池配置、异常处理。
- 最高风险点:增量同步延迟导致数据丢失,建议采用双写 + binlog订阅组合。
- 最佳实践:先在测试环境模拟完整迁移,包括流量回放和压力测试。
如果需要具体工具配置或代码实现细节(如Canal监听、Redis Cluster迁移脚本),可以进一步细化您的架构从而提供针对性方案。