Redis 叢集擴容、環境遷移、資料備份等場景常需在不同 Redis 實例/資料庫間複製資料。本文提供支援所有 Redis 資料類型 (String、List、Hash、Set、Sorted Set) 的完整遷移腳本,含進度顯示與錯誤處理。
<?php
/**
* Redis 資料遷移腳本
* 支援類型: String, List, Hash, Set, Sorted Set
*
* 使用方式:
* php migrate.php '127.0.0.1:6379/0' '127.0.0.1:6379/1'
*/
// 來源與目標連線字串格式: host:port/db
$from = $argv[1] ?? '127.0.0.1:6379/0';
$to = $argv[2] ?? '127.0.0.1:6379/1';
echo "遷移: {$from} → {$to}\n";
$fromRedis = redisInit($from);
$toRedis = redisInit($to);
// 掃描所有鍵 (使用 SCAN 避免阻塞)
$keys = [];
$iterator = null;
while ($keysBatch = $fromRedis->scan($iterator, '*', 1000)) {
$keys = array_merge($keys, $keysBatch);
if ($iterator === 0) break;
}
$total = count($keys);
$count = 0;
echo "發現 {$total} 個鍵,開始遷移...\n";
foreach ($keys as $key) {
$count++;
// 進度顯示
if ($count % 100 === 1 || $count === $total) {
echo "進度: {$count}/{$total} (" . number_format($count/$total*100, 1) "%)\n";
}
$type = $fromRedis->type($key);
try {
switch ($type) {
case Redis::REDIS_STRING:
$val = $fromRedis->get($key);
$toRedis->set($key, $val);
break;
case Redis::REDIS_LIST:
$list = $fromRedis->lRange($key, 0, -1);
if ($list) {
$toRedis->delete($key); // 確保目標不存在
foreach ($list as $val) {
$toRedis->rPush($key, $val);
}
}
break;
case Redis::REDIS_HASH:
$hash = $fromRedis->hGetAll($key);
if ($hash) {
$toRedis->hMSet($key, $hash);
}
break;
case Redis::REDIS_SET:
$members = $fromRedis->sMembers($key);
if ($members) {
$toRedis->sAdd($key, ...$members);
}
break;
case Redis::REDIS_ZSET:
$zset = $fromRedis->zRange($key, 0, -1, true); // true = with scores
if ($zset) {
foreach ($zset as $member => $score) {
$toRedis->zAdd($key, $score, $member);
}
}
break;
default:
echo "⚠️ 未知類型 {$type} 鍵: {$key},跳過\n";
}
} catch (Exception $e) {
echo "❌ 遷移失敗 [{$key}]: " . $e->getMessage() . "\n";
}
}
echo "✅ 遷移完成!共處理 {$count} 個鍵\n";
/**
* 建立 Redis 連線
* @param string $connStr 格式: host:port/db
* @return Redis
*/
function redisInit($connStr) {
$redis = new Redis();
// 解析連線字串
// 格式: host:port/db 或 host/db (預設 port 6379)
if (!preg_match('/^([^:]+)(?::(\d+))?(?:\/(\d+))?$/', $connStr, $matches)) {
throw new InvalidArgumentException("無效連線字串: {$connStr}");
}
$host = $matches[1];
$port = $matches[2] ?? 6379;
$db = $matches[3] ?? 0;
if (!$redis->connect($host, (int)$port)) {
throw new RuntimeException("無法連線 Redis: {$host}:{$port}");
}
if (!$redis->select((int)$db)) {
throw new RuntimeException("無法選擇資料庫: {$db}");
}
// 驗證連線
if ($redis->ping() !== '+PONG') {
throw new RuntimeException("Redis PING 失敗");
}
return $redis;
}
?>
<?php
// ❌ 不要在生產環境使用 KEYS * (阻塞單執行緒)
// $keys = $redis->keys('*');
// ✅ 使用 SCAN 迭代器 (非阻塞,游標式)
$iterator = null;
while ($batch = $redis->scan($iterator, '*', 1000)) {
// 處理批次
if ($iterator === 0) break; // 遊標歸零表示結束
}
// PHP Redis 擴充 5.3.0+ 支援 scan 回傳陣列直接
$keys = [];
$iterator = null;
do {
$batch = $redis->scan($iterator, '*', 1000);
$keys = array_merge($keys, $batch);
} while ($iterator !== 0);
?>
<?php
// 大量資料時使用 Pipeline 減少網路往返
$pipe = $toRedis->multi(Redis::PIPELINE);
foreach ($keys as $key) {
$type = $fromRedis->type($key);
switch ($type) {
case Redis::REDIS_STRING:
$pipe->set($key, $fromRedis->get($key));
break;
case Redis::REDIS_HASH:
$pipe->hMSet($key, $fromRedis->hGetAll($key));
break;
// ... 其他類型
}
// 每 1000 條執行一次
if (++$count % 1000 === 0) {
$pipe->exec();
$pipe = $toRedis->multi(Redis::PIPELINE);
}
}
$pipe->exec(); // 執行剩餘
?>
<?php
// 保留原鍵的過期時間
$ttl = $fromRedis->ttl($key);
if ($ttl > 0) {
$toRedis->expire($key, $ttl);
} elseif ($ttl === -1) {
$toRedis->persist($key); // 移除過期時間
}
// 更精確: 取得毫秒級 TTL (Redis 2.6+)
$pttl = $fromRedis->pttl($key);
if ($pttl > 0) {
$toRedis->pexpire($key, $pttl);
}
?>
<?php
function migrateLargeHash($fromRedis, $toRedis, $key, $chunkSize = 1000) {
$cursor = 0;
do {
// HSCAN 分批讀取
$data = $fromRedis->hScan($key, $cursor, '*', $chunkSize);
$cursor = $data[0];
$fields = $data[1];
if ($fields) {
$toRedis->hMSet($key, $fields);
}
} while ($cursor !== '0');
}
// List 大鍵同理使用 LRANGE 分批
function migrateLargeList($fromRedis, $toRedis, $key, $chunkSize = 1000) {
$len = $fromRedis->lLen($key);
for ($offset = 0; $offset < $len; $offset += $chunkSize) {
$chunk = $fromRedis->lRange($key, $offset, $offset + $chunkSize - 1);
if ($chunk) {
$toRedis->rPush($key, ...$chunk);
}
}
}
?>
| 工具 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 本腳本 | 可控性強、支援自定義邏輯、PHP 環境原生 | 需自行維護、單執行緒較慢 | 中小量遷移、特殊轉換需求 |
| redis-cli --rdb | 官方工具、二進制級備份、速度快 | 全量覆蓋、不支援增量、需檔案存取權 | 完整備份/還原、叢集搬遷 |
| redis-replication | 主從同步、增量同步、生產級 | 架構複雜、需重啟/重新配置 | 高可用架構、即時同步 |
| RedisShake | 阿里開源、多模式、效能極佳 | Go 編譯、學習曲線 | 大規模遷移、同步、備份 |