缓存系统迁移别急着一刀切把自建 Redis 主从迁到 Redis Cluster表面上只是更换连接地址实际会碰到数据分片、客户端协议、连接池和多键命令等一组变化。旧系统里可以执行的MGET、Lua 脚本或事务到了 Cluster 上可能因为键不在同一个 Hash Slot 而失败。即使命令兼容连接数、超时和重试策略也未必还能沿用。因此迁移计划不该只写“某日全量切换”。更稳妥的做法是把过程拆成数据准备、差异校验、读流量灰度、写路径切换和旧集群退出几个阶段。每一阶段都要有进入条件、观察指标和回退方式。具体门槛由业务的容量基线、数据语义和故障预算决定不能从别的项目照搬一组比例或时间。第一阶段双写代理与双向增量同步新集群需要先追平存量数据再持续接收增量变更。增量从哪里取得要看缓存的数据来源。如果 Redis 只是 MySQL 的派生缓存可以从数据库变更日志或业务事件重建如果 Redis 中保存了数据库没有的状态则需要单独设计复制、补偿和冲突规则不能把 AOF、Binlog 和业务双写混为同一种方案。双写可以放在业务服务、数据访问层或专用代理中。代理能集中治理但也会新增一跳和一个故障点应用内双写能掌握业务语义却要避免不同服务各写一套实现。无论放在哪里都要先回答三个问题旧端成功、新端失败时如何补偿重试是否幂等删除、过期和更新乱序怎么处理。异步同步的延迟应当持续监控但不能预先承诺为某个固定数量级。迁移期间还应明确哪一端是事实来源。双写不等于双主更不表示两端可以随意接受人工修改。没有单一权威源时同一个键在两端出现不同值校验工具也无法判断应该保留哪一个。第二阶段离线数据一致性校验与动态 Diff 审计切流前需要比较新旧两端但“值相等”只是最基础的一层。校验范围还包括键是否存在、数据类型、TTL、序列化版本以及业务允许的时间差。对大集群执行全量扫描会消耗 CPU、网络和客户端连接校验任务要限速、可暂停并记录扫描游标以免它本身影响线上服务。下面的函数只演示字符串键的值比较。真实工具还要分别处理redis.Nil、读取超时和两端错误并避免把完整缓存值写入日志因为值中可能包含用户数据。TTL 也不适合简单要求完全相等两次读取存在时间差异步复制也会带来偏差应按缓存语义设置容差。type MigrationDiffEngine struct { oldClient *redis.Client newClient *redis.ClusterClient } func (d *MigrationDiffEngine) VerifyKey(ctx context.Context, key string) (bool, error) { oldVal, err1 : d.oldClient.Get(ctx, key).Result() newVal, err2 : d.newClient.Get(ctx, key).Result() oldMissing : errors.Is(err1, redis.Nil) newMissing : errors.Is(err2, redis.Nil) if oldMissing || newMissing { return oldMissing newMissing, nil } if err1 ! nil || err2 ! nil { return false, fmt.Errorf(read error: oldErr%v, newErr%v, err1, err2) } if oldVal ! newVal { // 只记录键的摘要和差异类型不记录实际值 log.Warnf([DIFF_MISMATCH] KeyHash: %x, Type: value, sha256.Sum256([]byte(key))) return false, nil } return true, nil }这个片段没有检查数据类型和 TTL也没有覆盖 Hash、Set、List 等结构不能直接当成完整迁移工具。实际校验可以按键空间分层先核对总量和类型分布再对重要键全量比较对普通键按风险抽样发现差异后保留可追踪的键摘要、同步位点和错误类型。是否允许进入灰度应依据业务定义的差异容忍度和同步积压量而不是固定的天数或小数点位数。Redis Cluster 还要单独做命令兼容性检查。多键命令要求相关键位于同一 SlotHash Tag 可以让一组键落到同一 Slot但也可能形成热点。除了静态搜索MGET、MSET、事务和 Lua 脚本还应在测试环境用真实键样本运行关键路径。第三阶段读写分离切流与自适应 Fallback 降级如果业务允许旧端继续作为写入权威源可以先把一部分读请求路由到新集群同时保留原有写路径和同步链路。灰度比例应逐级调整每一级观察命中率、错误率、延迟、连接数、同步积压和差异结果。比例本身不是重点重点是同一个键能够稳定落到同一侧避免一次请求读新端、下一次又读旧端而掩盖问题。下面用键的哈希结果做稳定分桶。示例把两个 DAO 都显式传入并用floorMod避免Integer.MIN_VALUE在Math.abs后仍为负数。回退只适合读操作写操作若在新端执行结果不明确不能盲目再写旧端否则可能制造重复或顺序倒置。Component public class MigrationRoutingAspect { Autowired private DynamicConfig config; Autowired private LegacyRedisDao legacyDao; Autowired private ClusterRedisDao clusterDao; public Object routeReadOperation(String key) { int grayRatio Math.max(0, Math.min(100, config.getMigrationReadRatio())); int bucket Math.floorMod(key.hashCode(), 100); if (bucket grayRatio) { try { return clusterDao.get(key); } catch (Exception e) { Metrics.counter(migration.fallback.count).increment(); log.error(Cluster read failed, keyHash{}, fallback to legacy, Integer.toHexString(key.hashCode()), e); return legacyDao.get(key); } } return legacyDao.get(key); } }读流量全部落到新集群不代表迁移结束。停止旧端写入前要确认新端写入、过期、淘汰和故障恢复路径都经过验证并为仍在运行的旧版本应用留出兼容窗口。切换写权威源后旧端最好先转为只读或隔离保留一段由团队确定的观察期不要立刻销毁。回滚开关也必须经过演练若新端已经接收了独有写入回滚前需要先解决数据回灌和冲突所谓“一键回退”并不天然安全。中间件迁移上线验证 Checklist上线清单至少应覆盖这些事项用接近实际的命令和键分布做容量验证检查连接池上限、连接超时、命令超时与重试放大效应。盘点多键命令、Lua、事务、发布订阅和客户端版本确认 Cluster 模式下的语义与限制。校验新增、更新、删除、过期和缓存穿透等路径并把差异记录关联到同步位点。为每个灰度阶段写清停止条件和负责人。监控除了 Redis 延迟与错误还要覆盖应用线程池、下游数据库、网络和同步队列。演练配置回退和数据回灌区分“新端只提供读服务”与“新端已经接受写入”两种回滚难度。旧集群下线前确认没有遗留客户端、定时任务或运维脚本继续访问并保留经过审批的数据归档与销毁记录。缓存迁移的核心不是把切流按钮做得多快而是让每一步都有证据可以判断继续还是退回。只要权威数据源、兼容边界和回滚后的数据处理仍说不清就还没有到全量切换的时候。