大 key 100 MB 引发阻塞,如何无损拆分并保证事务一致性

解读

  1. 场景定位:国内互联网主流使用 Redis Cluster 做缓存,100 MB 的 String/Hash/Set 都会落在同一个 slot,导致
    • 单线程处理命令时阻塞 50–200 ms,拖慢整个分片
    • 主从同步、AOF rewrite、RDB 落盘时网卡/磁盘 IO 飙升,触发哨兵或集群“节点失联”误判
    • 业务侧超时率突增,SLA 被打穿
  2. 面试意图:考察候选人能否把“性能瓶颈”转化为“可落地的数据迁移方案”,并兼顾高并发下的原子性与回滚能力,而不是简单回答“用 unlink 删除”或“拆成多个 key”。

知识点

  1. Redis 单线程模型与“大 key 延迟”量化指标
  2. Redis 事务与 Lua 脚本原子性,以及 Cluster 环境下 “cross-slot” 限制
  3. 可重入锁 / 分布式锁(Redisson、RedLock)在迁移过程中的互斥作用
  4. 数据分片策略:Hash-tag、按业务维度二次分片、一致性哈希
  5. 无损迁移三板斧:双写 → 对比 → 灰度切换,以及回滚预案
  6. 国内常用中间件:
    • redis-full-check(阿里云开源)做数据校验
    • 企业版 Tair 的 “async-del” 能力或 “大 key 自动拆分” 开关
    • 自研代理层(如 Bilibili 的 “Overlord”)在迁移期做 key 路由
  7. 性能验证指标:P99 延迟、QPS 跌落幅度、主从复制 offset 差距、内存碎片率、网络出入流量

答案

核心思路:把“100 MB 单 key”拆成“N 个 ≤1 MB 的子 key”,迁移过程对业务“零感知”、可回滚、数据不丢不重。
步骤如下:

  1. 事前评估
    a. 用 redis-cli --bigkeysmemory usage 精确测量 key 大小、field 数量、访问热度
    b. 在测试环境回放真实流量,确认 100 MB key 的读取 QPS 与写入 QPS,记录 P99 延迟基线

  2. 设计拆分方案
    以 Hash 为例,假设原 key 为 user:profile,field 数量 400 万,平均 field 250 B,总 100 MB。
    按 “10000 字段/子 key” 切分,得到 400 个子 key:user:profile:1 … user:profile:400
    子 key 大小 ≈ 2.5 MB,远小于 8 MB 的“大 key 告警线”,且落在相同 Hash-tag {user:profile} 保证同 slot,避免 cross-slot 事务限制

  3. 迁移流程(基于双写 + 分布式锁)
    ① 上线“双写”代码:

    • 写操作:先写“老 key”,再写“子 key 列表”,两步包裹在 Lua 脚本里保证原子性
    • 读操作:优先读子 key,miss 时回源老 key 并异步回填
      ② 加分布式锁(Redisson,30 s 租约,可续期),禁止后台任务对老 key 做 delete/expire
      ③ 全量数据校验:用 redis-full-check 对比老 key 与所有子 key 的 field 数量、ttl、值摘要,差异率必须 0
      ④ 灰度切换:
    • 按用户尾号 1% → 10% → 100% 逐步把读流量切到子 key
    • 每步观察 5 分钟,P99 延迟上涨不超过 5%,错误日志为 0
      ⑤ 清理老 key:
    • 确认 24 h 内无回滚需求后,用 UNLINK 异步删除,避免阻塞
    • 释放分布式锁,下线双写代码
  4. 事务一致性保障

    • 双写阶段:Lua 脚本保证“老 key 和子 key 要么都写成功,要么都失败”,脚本内部用 pcall 捕获异常并回滚
    • 切换阶段:分布式锁确保“只有一台机器”执行流量切换脚本,防止并发路由不一致
    • 回滚预案:若灰度阶段发现子 key 延迟更高,可秒级把读标志位改回“老 key”,子 key 数据保留 48 h 后清理
  5. 性能验证

    • 在压测环境用 2 k QPS 写、10 k QPS 读持续 30 min,P99 延迟从 180 ms 降到 8 ms,CPU 利用率下降 35%
    • 生产灰度阶段,把老 key 的“慢查询日志”指标(latency>50ms 次数)从 120 次/min 降到 0

拓展思考

  1. 如果业务不允许停写,且 Redis 版本 ≤3.0(不支持 UNLINK),如何最小化删除大 key 的阻塞?
    可借助“渐进式删除”脚本:每次 HSCAN 出 1 000 字段,HDEL 后 sleep 10 ms,把 100 MB 拆成 4 000 批次,总耗时 40 s,峰值延迟控制在 5 ms 以内

  2. 若拆分的子 key 仍需跨 slot,如何用 RedLock 保证跨节点事务一致性?
    把“事务”降级为“幂等消息”:业务层生成 UUID 消息,写入 Redis Stream,消费端对子 key 做幂等 SET;通过消费位点做最终一致性校验,允许秒级延迟

  3. 国内云厂商 Redis 企业版提供“大 key 自动拆分”功能,性能测试同学如何评估其真实效果?
    设计“同等硬件规格”对比实验:

    • 变量:开启/关闭自动拆分开关
    • 指标:同等 5 k QPS 下,P99 延迟、主从复制流量、内存额外开销、故障恢复时间 RTO
    • 结论:若 P99 延迟下降 80%,但内存上涨 12%,需给出 SLA 容忍度建议,并写入性能基线报告