Redis 大 Key 治理复盘从一个 28MB 的 Hash 导致集群不可用说起一、Redis 集群的定时炸弹一个 28MB 的 Hash Key在一次常规的业务功能上线三周后Redis Cluster 集群开始出现间歇性的响应延迟。redis-cli --latency监测显示 P50 延迟从日常的 0.3ms 飙升到 220ms每 12 分钟出现一个持续 3~5 秒的延迟尖峰。更严重的是在延迟尖峰期间集群发生了两次主从切换——哨兵判断主节点超时无响应。排查日志发现每次延迟尖峰都与 Redis 的bgsaveRDB 持久化时间窗口重合。这说明bgsave的fork()操作在复制父进程的页表时发生了严重阻塞。但常规情况下fork()的耗时应该小于 1 秒——除非父进程占用了异常巨大的内存。使用redis-cli --bigkeys扫描后发现了元凶一个存储用户行为序列的 Hash Key包含 350 万个子字段总内存占用 28MB。这个大 Key导致了三个连锁问题fork 阻塞、主从同步延迟和集群迁移失败。二、大 Key 的四种形态与检测手段Redis 大 Key 的典型形态与排查方法# 1. 扫描大 Key生产环境慎用 --bigkeys会阻塞 redis-cli --bigkeys -i 0.1 # -i 0.1 每扫描 100 个 key 休眠 0.1 秒 # 2. 内存分析需要 redis-rdb-tools rdb -c memory dump.rdb --bytes 10240 --type hash big_keys.csv # 3. 实时监控慢查询 redis-cli slowlog get 100 # 4. 查看 key 的大小不阻塞但需要知道具体 key 名 redis-cli MEMORY USAGE user:behavior:uid_123456 # 5. 查看 key 内部元素数量 redis-cli HLEN user:behavior:uid_123456 # Hash redis-cli LLEN queue:events # List redis-cli SCARD tags:active_users # Set redis-cli ZCARD leaderboard:daily # Sorted Set四种最常见的大 Key 类型及其成因类型典型场景危险阈值风险大 Hash用户行为序列、配置聚合 5000 字段 或 10MBfork 阻塞、迁移失败大 List消息队列、事件流 10000 元素LPOP/RPOP 阻塞、备份慢大 Set用户标签集合 10000 成员SMEMBERS/SINTER 阻塞大 String缓存序列化对象 10KB 单个值带宽吃紧、反序列化慢三、大 Hash 的拆分方案从单 Key 到多层结构的渐进迁移针对 28MB、350 万字段的大 Hash进行分层拆分# 大 Hash 拆分脚本 —— 将单 Key 拆分为多级 Key 的渐进迁移方案 import redis from typing import Generator def split_large_hash(r: redis.Redis, source_key: str, batch_size: int 1000, bucket_count: int 100): 将大 Hash 拆分为 bucket_count 个子 Hash。 拆分策略 - 原 Key: user:behavior:uid_123456 - 新 Key: user:behavior:uid_123456:{0..99} - 路由: hash(user_id) % bucket_count 决定写入哪个子 Key cursor 0 migrated 0 errors 0 while True: # 渐进式扫描每次只取 batch_size 个字段不阻塞主线程 cursor, fields r.hscan(source_key, cursor, countbatch_size) for field, value in fields.items(): # 根据 field 的 hash 值路由到目标子 Key bucket hash(field) % bucket_count target_key f{source_key}:{bucket} try: # 使用 pipeline 批量操作减少网络往返 pipe r.pipeline(transactionFalse) pipe.hset(target_key, field, value) # 写入新 Key pipe.hdel(source_key, field) # 删除旧 Key 中字段 pipe.execute() migrated 1 except redis.RedisError as e: errors 1 # 记录失败的字段用于回滚或重试 log_error(source_key, field, str(e)) if cursor 0: break # 批次间休眠降低对集群的影响 time.sleep(0.05) return {migrated: migrated, errors: errors}对于需要同时支持读写的在线服务在迁移期间使用双写双读策略# 迁移期间的双写代理层 —— 保证数据一致性和可用性 class HashProxy: def __init__(self, redis_client, source_key: str, bucket_count: int 100): self.redis redis_client self.source_key source_key self.bucket_count bucket_count self.migration_complete False # 迁移完成后置为 True def hget(self, key: str, field: str) - Optional[bytes]: # 先查源 Key value self.redis.hget(self.source_key, field) if value is not None: return value # 源 Key 未命中已迁移部分查子 Key if not self.migration_complete: bucket hash(field) % self.bucket_count return self.redis.hget(f{self.source_key}:{bucket}, field) return None def hset(self, key: str, field: str, value: str): # 双写同时写入源 Key 和子 Key仅迁移期间 bucket hash(field) % self.bucket_count pipe self.redis.pipeline(transactionFalse) if not self.migration_complete: pipe.hset(self.source_key, field, value) pipe.hset(f{self.source_key}:{bucket}, field, value) pipe.execute()四、大 Key 预防机制从被动治理到主动拦截消除当前的大 Key 只是应急措施需要建立长效预防机制# 大 Key 预防 —— 写入前的元素数量阈值检查 class SafeRedisHash: MAX_FIELD_COUNT 5000 # 单 Hash 最大字段数阈值 def __init__(self, redis_client): self.redis redis_client def safe_hset(self, key: str, field: str, value: str): # 写入前检查当前字段数是否超阈值 field_count self.redis.hlen(key) if field_count self.MAX_FIELD_COUNT: # 触发告警并自动分桶 alert_large_key(key, field_count) bucket hash(field) % 10 key f{key}:overflow:{bucket} self.redis.hset(key, field, value) def safe_hset_batch(self, key: str, fields: dict): # 批量写入的预防检查预估写入后的总数 current self.redis.hlen(key) if current len(fields) self.MAX_FIELD_COUNT: raise LargeKeyPreventionError( f拒绝写入{key} 当前 {current} 字段 f本次 {len(fields)} 字段超出阈值 {self.MAX_FIELD_COUNT} ) self.redis.hmset(key, fields)治理前后的指标变化指标治理前治理后最大单 Key 大小28MB380KBBGSAVE fork 耗时3.2s80msP50 延迟0.3ms常态/ 220ms峰值0.3ms稳态主从同步延迟8.5s120ms哨兵误切次数/月20五、总结Redis 大 Key 治理的核心原则预防优于治理写入前检查是成本最低的方案在业务逻辑层就拦截大 Key 的产生比事后拆分节省数倍的工程时间大 Key 的破坏是连锁性的一个 28MB 的 Hash 会引起 fork 阻塞→哨兵误判→主从切换→数据不一致的连锁反应排查时需要追踪整条影响链渐进式迁移优于一次性迁移HSCAN 批次间休眠的方式对生产集群影响最小一次性 HGETALL DEL 会导致毫秒级的服务不可用每个 Redis 数据类型都有对应的大 Key 阈值Hash 5000 字段、List 10000 元素、String 10KB 是需要报警的参考线具体阈值应根据实例规格调整。排查工具链推荐redis-cli --bigkeys初步扫描→redis-rdb-tools --bytes精确分析→MEMORY USAGE key单 Key 诊断。
Redis 大 Key 治理复盘:从一个 28MB 的 Hash 导致集群不可用说起
Redis 大 Key 治理复盘从一个 28MB 的 Hash 导致集群不可用说起一、Redis 集群的定时炸弹一个 28MB 的 Hash Key在一次常规的业务功能上线三周后Redis Cluster 集群开始出现间歇性的响应延迟。redis-cli --latency监测显示 P50 延迟从日常的 0.3ms 飙升到 220ms每 12 分钟出现一个持续 3~5 秒的延迟尖峰。更严重的是在延迟尖峰期间集群发生了两次主从切换——哨兵判断主节点超时无响应。排查日志发现每次延迟尖峰都与 Redis 的bgsaveRDB 持久化时间窗口重合。这说明bgsave的fork()操作在复制父进程的页表时发生了严重阻塞。但常规情况下fork()的耗时应该小于 1 秒——除非父进程占用了异常巨大的内存。使用redis-cli --bigkeys扫描后发现了元凶一个存储用户行为序列的 Hash Key包含 350 万个子字段总内存占用 28MB。这个大 Key导致了三个连锁问题fork 阻塞、主从同步延迟和集群迁移失败。二、大 Key 的四种形态与检测手段Redis 大 Key 的典型形态与排查方法# 1. 扫描大 Key生产环境慎用 --bigkeys会阻塞 redis-cli --bigkeys -i 0.1 # -i 0.1 每扫描 100 个 key 休眠 0.1 秒 # 2. 内存分析需要 redis-rdb-tools rdb -c memory dump.rdb --bytes 10240 --type hash big_keys.csv # 3. 实时监控慢查询 redis-cli slowlog get 100 # 4. 查看 key 的大小不阻塞但需要知道具体 key 名 redis-cli MEMORY USAGE user:behavior:uid_123456 # 5. 查看 key 内部元素数量 redis-cli HLEN user:behavior:uid_123456 # Hash redis-cli LLEN queue:events # List redis-cli SCARD tags:active_users # Set redis-cli ZCARD leaderboard:daily # Sorted Set四种最常见的大 Key 类型及其成因类型典型场景危险阈值风险大 Hash用户行为序列、配置聚合 5000 字段 或 10MBfork 阻塞、迁移失败大 List消息队列、事件流 10000 元素LPOP/RPOP 阻塞、备份慢大 Set用户标签集合 10000 成员SMEMBERS/SINTER 阻塞大 String缓存序列化对象 10KB 单个值带宽吃紧、反序列化慢三、大 Hash 的拆分方案从单 Key 到多层结构的渐进迁移针对 28MB、350 万字段的大 Hash进行分层拆分# 大 Hash 拆分脚本 —— 将单 Key 拆分为多级 Key 的渐进迁移方案 import redis from typing import Generator def split_large_hash(r: redis.Redis, source_key: str, batch_size: int 1000, bucket_count: int 100): 将大 Hash 拆分为 bucket_count 个子 Hash。 拆分策略 - 原 Key: user:behavior:uid_123456 - 新 Key: user:behavior:uid_123456:{0..99} - 路由: hash(user_id) % bucket_count 决定写入哪个子 Key cursor 0 migrated 0 errors 0 while True: # 渐进式扫描每次只取 batch_size 个字段不阻塞主线程 cursor, fields r.hscan(source_key, cursor, countbatch_size) for field, value in fields.items(): # 根据 field 的 hash 值路由到目标子 Key bucket hash(field) % bucket_count target_key f{source_key}:{bucket} try: # 使用 pipeline 批量操作减少网络往返 pipe r.pipeline(transactionFalse) pipe.hset(target_key, field, value) # 写入新 Key pipe.hdel(source_key, field) # 删除旧 Key 中字段 pipe.execute() migrated 1 except redis.RedisError as e: errors 1 # 记录失败的字段用于回滚或重试 log_error(source_key, field, str(e)) if cursor 0: break # 批次间休眠降低对集群的影响 time.sleep(0.05) return {migrated: migrated, errors: errors}对于需要同时支持读写的在线服务在迁移期间使用双写双读策略# 迁移期间的双写代理层 —— 保证数据一致性和可用性 class HashProxy: def __init__(self, redis_client, source_key: str, bucket_count: int 100): self.redis redis_client self.source_key source_key self.bucket_count bucket_count self.migration_complete False # 迁移完成后置为 True def hget(self, key: str, field: str) - Optional[bytes]: # 先查源 Key value self.redis.hget(self.source_key, field) if value is not None: return value # 源 Key 未命中已迁移部分查子 Key if not self.migration_complete: bucket hash(field) % self.bucket_count return self.redis.hget(f{self.source_key}:{bucket}, field) return None def hset(self, key: str, field: str, value: str): # 双写同时写入源 Key 和子 Key仅迁移期间 bucket hash(field) % self.bucket_count pipe self.redis.pipeline(transactionFalse) if not self.migration_complete: pipe.hset(self.source_key, field, value) pipe.hset(f{self.source_key}:{bucket}, field, value) pipe.execute()四、大 Key 预防机制从被动治理到主动拦截消除当前的大 Key 只是应急措施需要建立长效预防机制# 大 Key 预防 —— 写入前的元素数量阈值检查 class SafeRedisHash: MAX_FIELD_COUNT 5000 # 单 Hash 最大字段数阈值 def __init__(self, redis_client): self.redis redis_client def safe_hset(self, key: str, field: str, value: str): # 写入前检查当前字段数是否超阈值 field_count self.redis.hlen(key) if field_count self.MAX_FIELD_COUNT: # 触发告警并自动分桶 alert_large_key(key, field_count) bucket hash(field) % 10 key f{key}:overflow:{bucket} self.redis.hset(key, field, value) def safe_hset_batch(self, key: str, fields: dict): # 批量写入的预防检查预估写入后的总数 current self.redis.hlen(key) if current len(fields) self.MAX_FIELD_COUNT: raise LargeKeyPreventionError( f拒绝写入{key} 当前 {current} 字段 f本次 {len(fields)} 字段超出阈值 {self.MAX_FIELD_COUNT} ) self.redis.hmset(key, fields)治理前后的指标变化指标治理前治理后最大单 Key 大小28MB380KBBGSAVE fork 耗时3.2s80msP50 延迟0.3ms常态/ 220ms峰值0.3ms稳态主从同步延迟8.5s120ms哨兵误切次数/月20五、总结Redis 大 Key 治理的核心原则预防优于治理写入前检查是成本最低的方案在业务逻辑层就拦截大 Key 的产生比事后拆分节省数倍的工程时间大 Key 的破坏是连锁性的一个 28MB 的 Hash 会引起 fork 阻塞→哨兵误判→主从切换→数据不一致的连锁反应排查时需要追踪整条影响链渐进式迁移优于一次性迁移HSCAN 批次间休眠的方式对生产集群影响最小一次性 HGETALL DEL 会导致毫秒级的服务不可用每个 Redis 数据类型都有对应的大 Key 阈值Hash 5000 字段、List 10000 元素、String 10KB 是需要报警的参考线具体阈值应根据实例规格调整。排查工具链推荐redis-cli --bigkeys初步扫描→redis-rdb-tools --bytes精确分析→MEMORY USAGE key单 Key 诊断。