Redis 主从迁 Cluster 4 周复盘:5 个大坑和真实数据

从 Redis 5.0 主从迁到 Redis 7.2 Cluster 6 主 6 从的全过程:redis-shake 在线迁移 + 客户端集群感知 + 5 大坑(hash 分布/Lua 跨槽/pipeline MOVED/scan 漏 key/慢查询阻塞)+ 监控告警 + 容量规划。容量 +75%,QPS +5x,故障切换 30s→5s。

2024 年 6 月,我们把一套 Redis 5.0 主从架构(3 主 3 从)迁到 Redis 7.2 Cluster(6 主 6 从),用了 4 周时间。期间踩了 5 个大坑:hash tag 分布不均、Lua 脚本跨槽、pipeline 报 MOVED、客户端集群感知不全、scan 漏 key。本文复盘整个迁移流程 + 真实数据 + 修法。

迁移背景

原架构:Redis 5.0 主从
- 3 个独立实例:cache-1 (用户 session)、cache-2 (商品)、cache-3 (订单)
- 每个 1 主 1 从,sentinel 选主
- 单实例 32GB,总容量 96GB
- QPS 峰值 8w/s(集群级)

痛点:
1. 容量碰天花板:cache-2 用了 30GB,扩容只能纵向(更大内存机器)
2. 单点 QPS 瓶颈:cache-1 session 写入 4w/s,主节点 CPU 80%
3. 主从切换还是慢(sentinel 30s 才完成 failover)
4. 业务读写分离逻辑乱(三个独立实例,key 怎么分散三处)

目标:
- 升 Redis 7.2(开启 CLUSTER 模式)
- 6 主 6 从,水平扩展
- 自动 sharding,业务无感
- 故障自动转移 < 5s

Redis Cluster 原理

Redis Cluster:
- 16384 个 slot,key 经过 CRC16(key) mod 16384 决定落哪个 slot
- 每个 master 负责一段连续 slot
- slave 复制 master,master 挂 slave 自动接管
- 客户端直连任意节点,节点返回 MOVED 重定向

数据分布(6 master):
- node-1: slot 0-2730
- node-2: slot 2731-5460
- node-3: slot 5461-8191
- node-4: slot 8192-10922
- node-5: slot 10923-13652
- node-6: slot 13653-16383

要点:
- 同一 hash slot 的 key 才能 MULTI / Lua / pipeline 批量
- 用 hash tag 强制聚合:user:{12345}:profile 和 user:{12345}:cart 都按 12345 算槽
- 跨槽操作:报 CROSSSLOT 错误

部署 Redis Cluster

# 6 节点(3 master + 3 slave 简化版,生产用 6+6)
# 每节点配置
$ cat /etc/redis/redis.conf
port 6379
cluster-enabled yes
cluster-config-file /var/lib/redis/nodes.conf
cluster-node-timeout 5000
cluster-require-full-coverage no  # 部分槽不可用时,其他槽继续服务
cluster-allow-reads-when-down no
appendonly yes
appendfsync everysec
maxmemory 28gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
tcp-keepalive 60

# 创建集群
$ redis-cli --cluster create \
    10.0.1.1:6379 10.0.1.2:6379 10.0.1.3:6379 \
    10.0.1.4:6379 10.0.1.5:6379 10.0.1.6:6379 \
    --cluster-replicas 1

# >>> Performing hash slots allocation on 6 nodes...
# Master[0] -> Slots 0 - 5460
# Master[1] -> Slots 5461 - 10922
# Master[2] -> Slots 10923 - 16383
# Adding replica 10.0.1.5:6379 to 10.0.1.1:6379
# ...
# [OK] All 16384 slots covered.

# 验证
$ redis-cli -h 10.0.1.1 cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_known_nodes:6
cluster_size:3

$ redis-cli -h 10.0.1.1 cluster nodes

数据迁移方案

不能直接停旧 Redis 再启新的,要"在线迁移"
方案对比:

方案 1:redis-shake(阿里开源)
- 从旧 Redis 拉 RDB + 同步增量
- 支持单机 → cluster 转换
- 我们用的这个

方案 2:双写
- 业务代码同时写新旧 Redis
- 老 Redis 数据迁移完后切读
- 改业务代码,适合长期迁移

方案 3:RIOT(Redis 迁移工具)
- 类似 redis-shake,但更现代

最终选 redis-shake,因为业务代码不动

redis-shake 配置

# shake.toml
[sync_reader]
address = "10.0.0.10:6379"     # 旧 Redis 主
username = ""
password = "oldpass"
tls = false
sync_rdb = true
sync_aof = true
prefer_replica = false          # 直接读主,避免延迟
try_diskless = true

[redis_writer]
address = "10.0.1.1:6379"      # 集群入口
username = ""
password = "newpass"
tls = false
cluster = true                  # 集群模式!
target_db = 0
target_redis_proto_max_bulk_len = 512mb

[filter]
allow_db = [0]
allow_keys = []                 # 空 = 全量
block_keys = ["__sentinel__*"]
allow_key_regex = []
block_key_regex = []
function = ""                   # Lua 自定义过滤

[advanced]
log_file = "shake.log"
log_level = "info"
log_interval = 5
rdb_restore_command_behavior = "rewrite"  # 已存在的 key 覆盖
pipeline_count_limit = 1024
target_redis_client_max_querybuf_len = 1gb
target_redis_proto_max_bulk_len = 512mb
aws_psync = ""
empty_db_before_sync = false    # 不要清空目标!

[module]
target_mbbloom_version = 20603
# 启动
$ ./redis-shake shake.toml

# 监控
$ tail -f shake.log
2024-06-10 14:23:01 [INFO] sync rdb 100% (8.2GB / 8.2GB)
2024-06-10 14:23:15 [INFO] aof sync: lag=2ms, qps=4500
2024-06-10 14:23:20 [INFO] aof sync: lag=1ms, qps=4800

# lag 持续 < 100ms 才算追平,可以切流量

客户端改造

// 旧代码:单机连接
import "github.com/redis/go-redis/v9"

rdb := redis.NewClient(&redis.Options{
    Addr:     "cache-1:6379",
    Password: "oldpass",
})

// 新代码:Cluster Client
rdb := redis.NewClusterClient(&redis.ClusterOptions{
    Addrs: []string{
        "10.0.1.1:6379",
        "10.0.1.2:6379",
        "10.0.1.3:6379",
    },
    Password: "newpass",

    // 关键参数
    MaxRedirects:   3,                  // MOVED 重定向最多 3 次
    ReadOnly:       false,
    RouteByLatency: false,              // 不按延迟路由
    RouteRandomly:  false,

    PoolSize:        100,                // 每节点连接池
    MinIdleConns:    10,
    PoolTimeout:     5 * time.Second,
    ReadTimeout:     500 * time.Millisecond,
    WriteTimeout:    500 * time.Millisecond,
    DialTimeout:     2 * time.Second,
})

// 验证
ctx := context.Background()
err := rdb.Ping(ctx).Err()
if err != nil {
    log.Fatalf("connect failed: %v", err)
}

// 集群所有节点 ping
err = rdb.ForEachShard(ctx, func(ctx context.Context, shard *redis.Client) error {
    return shard.Ping(ctx).Err()
})

坑 1:hash 分布不均

迁移完发现:
- node-1: 28GB(满了!)
- node-2: 12GB
- node-3: 15GB

原因:业务 key 大量带 {user_session},全部聚到一个 slot

排查:
$ redis-cli --cluster check 10.0.1.1:6379

[WARNING] Node 10.0.1.1:6379 has 19283482 keys in 5461 slots, but slot 1234 alone has 8923012 keys
# 找出热点 slot
$ redis-cli -h 10.0.1.1 --hotkeys

# 找出 slot 中 key 数最多的
$ redis-cli -h 10.0.1.1 cluster countkeysinslot 1234
8923012

# 看具体 key 模式
$ redis-cli -h 10.0.1.1 cluster getkeysinslot 1234 100
1) "user:{session_v2}:abc..."
2) "user:{session_v2}:def..."
# 所有 session 都用了 {session_v2} hash tag,全部聚到 1234 slot!

# 修复:业务改用 {user_id} 做 hash tag
# 原 key: user:{session_v2}:abc → user:{12345}:session
# 这样按 user_id 分散到各 slot

坑 2:Lua 脚本跨槽报错

业务有库存扣减脚本:
local stock = tonumber(redis.call('GET', KEYS[1]))
local order_id = ARGV[1]
if stock and stock > 0 then
    redis.call('DECR', KEYS[1])
    redis.call('SET', KEYS[2], order_id)
    return 1
end
return 0

调用:eval LUA 2 stock:1001 order:99999 user-001

报错:CROSSSLOT Keys in request don't hash to the same slot

原因:stock:1001 和 order:99999 不在同一 slot
# 修法:用 hash tag 强制同槽
# stock:{1001}:goods 和 order:{1001}:99999 → 按 1001 算槽

# 改业务代码,KEYS 都带 {goods_id}:
eval LUA 2 stock:{1001}:value order:{1001}:99999 user-001

# 也可以用 redis-cli 验证 slot
$ redis-cli cluster keyslot 'stock:{1001}:value'
6788
$ redis-cli cluster keyslot 'order:{1001}:99999'
6788   # 同槽,Lua 可执行

坑 3:pipeline 频繁 MOVED

// 旧代码:pipeline 100 个 key 一批
pipe := rdb.Pipeline()
for _, key := range keys {
    pipe.Get(ctx, key)
}
results, err := pipe.Exec(ctx)

// 集群下:100 个 key 可能落 6 个不同节点
// pipeline 串行执行每个 key,每个不在本节点的都 MOVED 重定向
// 延迟从 1ms 飙到 30ms
// 修法 1:按 slot 分批
import "github.com/redis/go-redis/v9"

func batchGet(keys []string) (map[string]string, error) {
    // 按 slot 分组
    slotKeys := make(map[int][]string)
    for _, k := range keys {
        slot := rdb.PoolStats().Hits  // 实际用 CRC16
        slotKeys[crc16Slot(k)] = append(slotKeys[crc16Slot(k)], k)
    }

    result := make(map[string]string)
    for _, ks := range slotKeys {
        pipe := rdb.Pipeline()
        cmds := make([]*redis.StringCmd, 0, len(ks))
        for _, k := range ks {
            cmds = append(cmds, pipe.Get(ctx, k))
        }
        pipe.Exec(ctx)
        for i, k := range ks {
            v, _ := cmds[i].Result()
            result[k] = v
        }
    }
    return result, nil
}

// 修法 2:用 mset/mget 时强制同 slot
// keys 必须带相同 hash tag
rdb.MGet(ctx, "user:{123}:name", "user:{123}:email")

坑 4:scan 漏 key

业务用 SCAN 0 MATCH 'session:*' COUNT 1000 遍历所有 session
集群下:SCAN 只扫当前连接的节点,需要遍历所有 master

修法:用客户端的 cluster scan
// 客户端实现 cluster 范围 scan
func clusterScan(pattern string) ([]string, error) {
    var allKeys []string

    err := rdb.ForEachMaster(ctx, func(ctx context.Context, master *redis.Client) error {
        var cursor uint64
        for {
            keys, c, err := master.Scan(ctx, cursor, pattern, 1000).Result()
            if err != nil {
                return err
            }
            allKeys = append(allKeys, keys...)
            cursor = c
            if cursor == 0 {
                break
            }
        }
        return nil
    })

    return allKeys, err
}

// 注意:scan 不能保证一致性视图(节点间数据有迁移)
// 关键业务不要用 scan,改用业务字典(MySQL/独立索引)

坑 5:慢查询阻塞集群

某天突然集群整体超时
排查发现一个节点 KEYS * 把整个 Redis 卡住
KEYS / SMEMBERS 大集合 / SORT / LRANGE 大列表都是危险操作

防御:
1. ACL 禁用 KEYS / FLUSHALL / FLUSHDB(普通业务账号)
2. 慢查询告警:slowlog-log-slower-than 10ms
3. 大 key 监控:redis-cli --bigkeys
# 创建受限账号
$ redis-cli -h 10.0.1.1
> ACL SETUSER appuser \
    on \
    >appsecret \
    ~* &* \
    +@all -@dangerous -KEYS -FLUSHALL -FLUSHDB -DEBUG -SHUTDOWN -CONFIG -CLUSTER \
    +CLUSTER|NODES +CLUSTER|INFO

# 慢查询配置
> CONFIG SET slowlog-log-slower-than 10000   # 10ms (单位 us)
> CONFIG SET slowlog-max-len 1024

# 查慢查询
> SLOWLOG GET 10

# 大 key 扫描
$ redis-cli -h 10.0.1.1 --bigkeys -i 0.01

# Biggest string found 'session:large:user:8888' has 12345 bytes
# Biggest hash   found 'user_profile:12345' has 8231 fields
# Biggest list   found 'event_log' has 1283910 items     ← 超大,要拆!
# Biggest set    found 'online_users' has 920000 members
# Biggest zset   found 'leaderboard' has 50000 members

故障转移测试

# 主动 kill 一个 master,观察 failover
$ kill -9 $(pgrep -f 'redis-server.*6379' | head -1)

# 监控
$ while true; do
    redis-cli -h 10.0.1.1 cluster info | grep cluster_state
    redis-cli -h 10.0.1.1 cluster nodes | grep -E 'master|slave' | head
    sleep 1
  done

# 时间线:
# t=0  : kill master 1
# t=1s : 其他节点 PFAIL master 1
# t=3s : quorum 达成 FAIL
# t=4s : slave 发起选主投票
# t=5s : slave 升 master,接管 slot
# t=6s : 客户端缓存的 MOVED 路由更新

# 实际业务影响:5-6 秒部分 key 不可读写

监控告警

# prometheus rules
groups:
  - name: redis_cluster
    rules:
      # 节点离线
      - alert: RedisClusterNodeDown
        expr: redis_up{job="redis-cluster"} == 0
        for: 30s
        labels: { severity: critical }
        annotations:
          summary: "Redis node {{ $labels.instance }} down"

      # 集群状态异常
      - alert: RedisClusterNotOK
        expr: redis_cluster_enabled == 1 and redis_cluster_state != 1
        for: 1m
        labels: { severity: critical }

      # 内存使用率高
      - alert: RedisMemoryHigh
        expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
        for: 5m
        labels: { severity: warning }

      # 主从复制延迟大
      - alert: RedisReplicationLag
        expr: redis_connected_slaves > 0
              and redis_replication_lag_seconds > 10
        for: 5m
        labels: { severity: warning }

      # 命中率低
      - alert: RedisHitRatioLow
        expr: redis_keyspace_hits_total / (redis_keyspace_hits_total + redis_keyspace_misses_total) < 0.7
        for: 10m
        labels: { severity: info }

      # slot 不全
      - alert: RedisClusterSlotsNotCovered
        expr: redis_cluster_slots_assigned < 16384
        for: 30s
        labels: { severity: critical }

容量规划

单 master 推荐配置:
- 内存:25-30GB(超过 30GB BGSAVE fork 慢)
- 集群规模:< 1000 master(gossip 流量 O(N²))
- 单节点 QPS:5w-8w(混合读写)
- 网络:万兆(主从复制 + cluster bus)

我们当前:
- 6 master × 28GB = 168GB
- 6 slave × 28GB
- 集群总 QPS:50w(读 80% slave 分担)
- 单节点峰值 12w QPS

下次扩容:
1. 添加 3 个新 master 节点
2. redis-cli --cluster reshard 重分片 slot
3. 监控搬迁进度
4. 全程业务不中断

迁移后效果

指标             迁移前(单机)    迁移后(Cluster)    变化
==========================================================
总容量           96GB             168GB                +75%
峰值 QPS         8w               50w                  +5x
故障切换         30s (sentinel)   5s (cluster)         -83%
单 key 延迟 p99  4ms              2ms                  -50%
扩容方式         纵向(换机器)     横向(加节点)

业务影响:
- 大促期间 Redis 不再是瓶颈
- 新业务直接用 Redis,容量不愁
- 旧业务 hash tag 改造一周完成

避坑清单

  1. 业务 key 设计要考虑 hash tag,避免热点 slot
  2. Lua / pipeline / MULTI 必须同 slot,用 {} 强制
  3. 慢操作(KEYS / FLUSHALL / 大集合操作)用 ACL 禁掉
  4. 客户端要用集群感知 client(go-redis / lettuce 等)
  5. scan 不要用于关键业务,集群下不保证一致性
  6. cluster-node-timeout 不要太短(< 5s 容易抖动)
  7. 主从复制延迟监控,> 10s 告警
  8. 大 key(string > 10KB, list/set > 5000)拆分
  9. maxmemory-policy 用 allkeys-lru 而非 noeviction
  10. 定期 redis-cli --cluster check 验证集群健康

总结

Redis Cluster 是水平扩容的最优解,但不是"换上就完事":业务代码、key 设计、客户端选型都要配合。我们这次迁移花了 4 周,实际改业务代码 1 周,剩下 3 周在调 hash tag + 验证 + 灰度。最重要的认知:Redis Cluster 不是"自动 sharding 黑盒",业务必须理解 slot/hash tag 才能用好。如果业务 key 模式不适合分片(大量跨 key 事务),先评估能否改造,再考虑迁移 — 不要为了 cluster 而 cluster。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

KEDA 自动扩缩落地实战:K8s 从 cron 扩缩到事件驱动

2026-5-19 12:18:44

技术教程

MongoDB 4.4 升 7.0 + 副本集变分片实录:6 个真实坑

2026-5-19 12:24:20

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索