Redis 大 Key 阻塞排查:一个 200MB 的 Hash 拖垮了整个集群
引言
周三下午 2 点,运营群反馈"用户行为分析看板打不开了"。运维查 Redis 监控:CPU 正常、内存正常、连接数正常,但命令延迟 P99 从 2ms 飙到 800ms——看起来一切正常却慢得离谱。接下来 10 分钟,越来越多的接口开始超时,告警从 3 条涨到 47 条,最终整个 Redis 集群不可用。
排查发现:一个名叫 user:behavior:all 的 Hash Key,存了全量用户行为日志,200MB。更致命的是,运维在试图 DEL 这个 Key 时,Redis 主线程被阻塞了 3 秒——3 秒内所有命令排队等待,集群雪崩。
根因出奇简单:业务把用户行为日志全塞进一个 Hash,没设过期时间,日积月累从 2KB 长到 200MB。Redis 是单线程的——一个慢命令阻塞主线程,整个实例都卡住。这篇文章从发现到修复完整复盘,附大 Key 扫描脚本和预防机制。
一、为什么一个大 Key 能拖垮整个集群
1.1 Redis 单线程模型
Redis 主线程(单线程处理所有命令)
│
├─ 命令1: GET user:1001 0.01ms
├─ 命令2: SET token:abc ... 0.02ms
├─ 命令3: DEL user:behavior:all 3000ms ← 阻塞 3 秒!
├─ 命令4: GET order:2001 ← 等了 3 秒才开始执行
├─ 命令5: HGET user:2002 ... ← 等了 3 秒
└─ ... 后面所有命令都延迟 3 秒
Redis 的单线程模型意味着:所有命令在主线程串行执行。一个命令耗时 3 秒,后面所有命令都要等 3 秒——不只是这个 Key 的请求慢,整个实例的所有 Key 的请求都慢。
1.2 大 Key 的危害链
| 危害 | 机制 | 表现 |
|---|---|---|
| DEL 阻塞 | DEL 是同步操作,大 Key 删除时主线程阻塞 | 删一个 200MB Hash 阻塞 3s |
| 网络阻塞 | 大 Value 传输占满网卡带宽 | HGETALL 返回 200MB,网络 100ms+ |
| 内存抖动 | 大 Key 删除后内存碎片率飙升 | used_memory_rss 不降,碎片率 > 2.0 |
| Cluster 迁移卡死 | 集群扩缩容时 slot 迁移搬大 Key | migrate 命令超时,slot 迁移卡住 |
| AOF 重写卡顿 | 大 Key 写入 AOF 时 fsync 慢 | AOF 重写期间延迟毛刺 |
| 雪崩放大 | 阻塞 → 客户端超时重试 → 更大并发 → 更堵 | 超时从 800ms → 5s → 30s |
1.3 什么样的 Key 算"大 Key"
| 数据类型 | 大 Key 阈值 | 说明 |
|---|---|---|
| String | > 10KB | 单个 Value 超过 10KB |
| Hash/List/Set/ZSet | > 5MB 或 > 10000 个 field | 体积或元素数量任一超标 |
| 任何类型 | > 10MB | 绝对值警戒线 |
| Stream | > 10MB 或 > 10000 条 | 消费者读取卡顿 |
200MB 的 Hash 属于极端大 Key——每个 field 存一条用户行为日志,约 50 万个 field,DEL 时主线程要遍历 50 万个 entry 逐个释放内存。
二、排查过程
2.1 第一步:确认是不是大 Key 引起的
# 方法1:redis-cli --bigkeys(采样扫描,对生产安全)
redis-cli -h <redis-host> -p <redis-port> -a <password> --bigkeys
# 输出示例:
# Sampling 100000 keys from keyspace...
#
# Biggest string found 'user:behavior:all' has 209715200 bytes (200MB)
# Biggest hash found 'user:behavior:all' has 524288 field
#
# Warning: 'user:behavior:all' is unusually large for a hash
# 方法2:memory usage(精确查单个 Key 内存占用)
redis-cli -h <redis-host> -p <redis-port> -a <password>
> MEMORY USAGE user:behavior:all
(integer) 209715200 # 200MB
# 方法3:查元素数量
> HLEN user:behavior:all
(integer) 524288 # 52 万个 field
# 方法4:查类型和 TTL
> TYPE user:behavior:all
hash
> TTL user:behavior:all
(integer) -1 # 没设过期时间!永久存在
2.2 --bigkeys 原理与局限
--bigkeys 的工作原理:
① 从所有 Key 中随机采样 N 个(默认按 --sample-keys 配置)
② 对每个采样的 Key 执行 TYPE + SIZE 操作(STRLEN/LLEN/HLEN/SCARD/ZCARD)
③ 统计每种类型最大的 Key
局限:
- 只采样,不全量扫描——如果大 Key 不在采样范围里就发现不了
- 只看元素数量,不看内存体积——一个 field 存 1MB 的 Hash 只有 1 个 field 也不算"大"
- 不报 TTL——不会告诉你这个大 Key 有没有过期时间
生产建议:--bigkeys 做日常巡检(采样快速),全量扫描用脚本(见第五章)做定期深度检查。
2.3 第二步:确认 DEL 阻塞
# 用 INFO stats 的 total_system_time 观察主线程阻塞
> INFO stats
# ...
# latest_fork_usec:1234 # 最近一次 fork 耗时(微秒)
# ...
# 如果 latest_fork_usec 或延迟指标异常,说明主线程被阻塞过
# 更直接的方法:slowlog
> SLOWLOG GET 10
1) 1) (integer) 14 # 日志 ID
2) (integer) 1694501234 # 时间戳
3) (integer) 3120000 # 执行耗时(微秒)= 3.12 秒!
4) 1) "DEL" # 命令
2) "user:behavior:all" # Key 名
# ...
# slowlog 里有一条 DEL 耗时 3.12 秒——实锤了
2.4 第三步:定位根因
# 抽样看这个 Hash 里存了什么
> HSCAN user:behavior:all 0 COUNT 10
1) "0"
2) 1) "user:10001:20250901"
2) "{\"action\":\"click\",\"page\":\"home\",\"ts\":1725148800}"
3) "user:10001:20250902"
4) "{\"action\":\"view\",\"page\":\"product\",\"ts\":1725235200}"
...
# 结论:用户行为日志全塞进一个 Hash
# Key = userId:日期,Value = JSON 行为记录
# 没设过期时间,日志天天累积,长到 200MB
2.5 事故时间线
| 时间 | 事件 | 影响 |
|---|---|---|
| 14:00 | 用户行为 Hash 增长到 200MB | 内存正常(集群有 16GB) |
| 14:05 | 运维发现延迟告警,尝试 DEL 删除 | 主线程阻塞 3s |
| 14:05:03 | 阻塞期间所有命令排队 | P99 从 2ms → 3s |
| 14:06 | 客户端超时重试,连接数翻倍 | 雪崩开始 |
| 14:10 | 超时从 3s → 30s | 47 条告警 |
| 14:15 | 用 UNLINK 替代 DEL 异步删除 | 主线程不再阻塞 |
| 14:18 | 集群恢复 | P99 回到 2ms |
三、修复方案
3.1 紧急止血:UNLINK 替代 DEL
# ❌ 危险:DEL 是同步删除,大 Key 阻塞主线程
DEL user:behavior:all
# ✅ 安全:UNLINK 是异步删除,主线程立即返回
UNLINK user:behavior:all
# 返回 (integer) 1,主线程几乎无阻塞
# 实际删除在后台线程(bio 后台异步删除线程)执行
| 删除方式 | 机制 | 阻塞主线程 | 适用 |
|---|---|---|---|
DEL | 同步释放内存 | 是 | 小 Key |
UNLINK | 异步释放内存(Redis 4.0+) | 否 | 大 Key |
SCAN + 逐个删除 | 分批删除 field | 每批短暂阻塞 | 超大 Key(GB 级) |
lazyfree-lazy-expire | 过期删除走异步 | 否 | 配置项全局生效 |
3.2 超大 Key 分批删除脚本
#!/bin/bash
# safe-delete-bigkey.sh
# 安全删除大 Hash Key:逐 field 删除,避免主线程阻塞
# 用法: ./safe-delete-bigkey.sh <redis-host> <port> <password> <key-name>
REDIS_HOST=$1
REDIS_PORT=$2
REDIS_PASS=$3
KEY_NAME=$4
BATCH_SIZE=100 # 每批删除 100 个 field
SLEEP_MS=50 # 每批之间 sleep 50ms 给主线程喘息
echo "[INFO] 开始安全删除大 Key: $KEY_NAME"
while true; do
# 用 HSCAN 取一批 field(不阻塞主线程)
FIELDS=$(redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS \
--no-auth-warning HSCAN $KEY_NAME 0 COUNT $BATCH_SIZE)
# 提取 field 名(HSCAN 返回格式:cursor field1 value1 field2 value2 ...)
CURSOR=$(echo "$FIELDS" | head -1)
if [ "$CURSOR" == "0" ] && [ $(echo "$FIELDS" | wc -l) -le 1 ]; then
echo "[INFO] Key 已空,执行最终 UNLINK"
redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS --no-auth-warning UNLINK $KEY_NAME
break
fi
# 逐个 HDEL 删除 field
FIELD_LIST=$(echo "$FIELDS" | tail -n +2 | awk 'NR%2==1')
for field in $FIELD_LIST; do
redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS --no-auth-warning HDEL $KEY_NAME "$field" > /dev/null
done
echo "[INFO] 已删除 $BATCH_SIZE 个 field,cursor=$CURSOR"
sleep 0.05 # 50ms
done
echo "[INFO] 删除完成"
3.3 根因修复:大 Key 拆分(Hash 按时间分片)
/**
* 大 Key 拆分:用户行为日志按天分片
*
* 拆分前:user:behavior:all(一个 200MB Hash,52 万 field)
* 拆分后:user:behavior:20250901(每天一个 Hash,~2000 field,~800KB)
* user:behavior:20250902
* user:behavior:20250903
* ...
*/
@Service
@RequiredArgsConstructor
public class UserBehaviorService {
private final StringRedisTemplate redis;
private static final int EXPIRE_DAYS = 30; // 每天数据保留 30 天
/**
* 记录用户行为:写入当天的分片 Hash
*/
public void recordBehavior(Long userId, String date, BehaviorLog log) {
String key = "user:behavior:" + date; // user:behavior:20250912
String field = userId + ":" + System.currentTimeMillis();
redis.opsForHash().put(key, field, JsonUtils.toJson(log));
// 设过期时间(只在 Key 第一次创建时设)
redis.expire(key, Duration.ofDays(EXPIRE_DAYS));
}
/**
* 查询某天用户行为
*/
public List<BehaviorLog> queryByDate(String date) {
String key = "user:behavior:" + date;
List<Object> values = redis.opsForHash().values(key);
return values.stream()
.map(v -> JsonUtils.fromJson((String) v, BehaviorLog.class))
.toList();
}
/**
* 查询跨天范围(多 Key 聚合,每个 Key 都小)
*/
public List<BehaviorLog> queryRange(String startDate, String endDate) {
List<BehaviorLog> result = new ArrayList<>();
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String key = "user:behavior:" + date.toString().replace("-", "");
// 每个 Key 只有几百 KB,HGETALL 不会阻塞
Map<Object, Object> entries = redis.opsForHash().entries(key);
entries.values().forEach(v ->
result.add(JsonUtils.fromJson((String) v, BehaviorLog.class))
);
}
return result;
}
}
3.4 拆分前后对比
| 维度 | 拆分前 | 拆分后 |
|---|---|---|
| Key 数量 | 1 个 | 30 个(按天分片) |
| 单 Key 体积 | 200MB | ~800KB |
| 单 Key field 数 | 52 万 | ~2000 |
| HGETALL 延迟 | 800ms(网络+序列化) | < 5ms |
| DEL 阻塞 | 3s | < 1ms |
| 过期清理 | 需手动 | TTL 自动过期 |
| Cluster 迁移 | 卡死 | 无影响 |
四、预防机制
4.1 全局配置:异步删除
# redis.conf
lazyfree-lazy-eviction yes # 内存淘汰时异步删除
lazyfree-lazy-expire yes # Key 过期时异步删除
lazyfree-lazy-server-del yes # 服务端内部删除(如 RENAME 覆盖旧 Key)异步
lazyfree-lazy-user-del yes # 用户执行 DEL 时也走异步(Redis 7.0+)
replica-lazy-flush yes # 主从同步时全量同步异步删除
这五条配置是预防大 Key 阻塞的第一道防线——即使出现大 Key,删除/过期/淘汰都不会阻塞主线程。
4.2 客户端规范:代码层防大 Key
| 规范 | 说明 |
|---|---|
| 单 Value 限制 | String 类型 ≤ 10KB,超过用 Hash 拆分 |
| 单 Hash field 限制 | ≤ 10000 个 field,超过按时间/用户分片 |
| 必设 TTL | 所有 Key 必须设过期时间,禁止无 TTL 的 Key |
| 禁止大结果集 | HGETALL/LRANGE 0 -1/SMEMBERS 禁用在大集合上,用 HSCAN/LRANGE 分页 |
| 写入前检查 | SET/HSET 前检查 Value 大小,超限拒绝写入 |
4.3 监控告警
# 告警规则:单 Key 内存超过阈值
# 每日扫描所有 Key,发现 > 10MB 的 Key 告警
# 方式1:MEMORY USAGE 逐个检查(精确但慢)
redis-cli -h <host> -p <port> --scan --pattern '*' | while read key; do
size=$(redis-cli -h <host> -p <port> MEMORY USAGE "$key" 2>/dev/null)
if [ "$size" -gt 10485760 ]; then # > 10MB
echo "[WARN] Big key: $key = $size bytes"
fi
done
# Prometheus + Grafana 告警
# redis_exporter 指标
- alert: RedisBigKeyDetected
expr: redis_key_size_bytes > 10485760 # 单 Key > 10MB
for: 5m
annotations:
summary: "Redis 大 Key 检测: {{ $labels.key }} = {{ $value }} bytes"
action: "检查该 Key 是否需要拆分或设置 TTL"
- alert: RedisSlowCommand
expr: redis_slowlog_last_id > redis_slowlog_last_id_offset # slowlog 有新条目
for: 1m
annotations:
summary: "Redis slowlog 新增慢命令"
五、大 Key 扫描脚本
5.1 全量扫描脚本
#!/bin/bash
# scan-bigkeys.sh
# 全量扫描 Redis 大 Key(不遗漏,不阻塞)
# 用法: ./scan-bigkeys.sh <redis-host> <port> <password> [size-threshold-bytes]
REDIS_HOST=${1:-127.0.0.1}
REDIS_PORT=${2:-6379}
REDIS_PASS=${3:-}
THRESHOLD=${4:-10485760} # 默认 10MB
BATCH=1000 # SCAN 每批数量
CLI="redis-cli -h $REDIS_HOST -p $REDIS_PORT -a $REDIS_PASS --no-auth-warning --raw"
echo "=========================================="
echo " Redis 大 Key 全量扫描"
echo " 阈值: $(numfmt --to=iec $THRESHOLD)"
echo " 开始: $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
printf "%-60s %-10s %-10s %-10s\n" "KEY" "TYPE" "SIZE" "TTL"
printf "%-60s %-10s %-10s %-10s\n" "---" "---" "---" "---"
CURSOR=0
while true; do
# SCAN 不阻塞主线程
RESULT=$($CLI SCAN $CURSOR COUNT $BATCH)
CURSOR=$(echo "$RESULT" | head -1)
KEYS=$(echo "$RESULT" | tail -n +2)
for key in $KEYS; do
[ -z "$key" ] && continue
TYPE=$($CLI TYPE "$key")
# 根据类型查大小
case "$TYPE" in
string) SIZE=$($CLI STRLEN "$key") ;;
hash) SIZE=$($CLI HLEN "$key") ;;
list) SIZE=$($CLI LLEN "$key") ;;
set) SIZE=$($CLI SCARD "$key") ;;
zset) SIZE=$($CLI ZCARD "$key") ;;
stream) SIZE=$($CLI XLEN "$key") ;;
none) SIZE=0 ;;
*) SIZE=0 ;;
esac
# 查内存占用(比元素数更精确)
MEM_BYTES=$($CLI MEMORY USAGE "$key" 2>/dev/null)
[ -z "$MEM_BYTES" ] && MEM_BYTES=0
# 查 TTL
TTL=$($CLI TTL "$key")
# 超过阈值才打印
if [ "$MEM_BYTES" -gt "$THRESHOLD" ]; then
MEM_HUMAN=$(numfmt --to=iec "$MEM_BYTES")
printf "%-60s %-10s %-10s %-10s\n" "$key" "$TYPE" "$MEM_HUMAN" "${TTL}s"
fi
done
# SCAN 到 0 表示扫描完成
[ "$CURSOR" == "0" ] && break
done
echo "=========================================="
echo " 扫描完成: $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
5.2 扫描输出示例
==========================================
Redis 大 Key 全量扫描
阈值: 10MiB
开始: 2025-09-12 14:30:00
==========================================
KEY TYPE SIZE TTL
--- --- --- ---
user:behavior:all hash 200MiB -1s
order:detail:cache:202509 hash 52MiB -1s
session:active:users set 18MiB 3600s
log:access:20250912 list 12MiB -1s
==========================================
扫描完成: 2025-09-12 14:32:15
==========================================
# 4 个大 Key,其中 2 个 TTL=-1(无过期),最高优先级处理
5.3 定时扫描 Cron
# crontab -e
# 每天凌晨 3 点扫描大 Key,结果发钉钉告警
0 3 * * * /opt/scripts/scan-bigkeys.sh redis-prod 6379 "$REDIS_PASS" 10485760 | \
grep -E "^user|^order|^log" | \
while read line; do
curl -s -X POST "https://oapi.dingtalk.com/robot/send?access_token=$DING_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[Redis大Key告警] $line\"}}"
done
六、常见问题
6.1 UNLINK 和 DEL 有什么区别?
DEL 是同步删除——主线程在删除完成前不处理其他命令。大 Key 的内存释放涉及遍历所有元素、逐个释放,200MB 的 Hash 可能阻塞 3 秒。UNLINK(Redis 4.0+)是异步删除——主线程只做"从键空间摘除 Key"这一步(O(1)),实际的内存释放在后台线程(bio 后台线程)异步执行。主线程几乎无阻塞。但 UNLINK 不是万能的——后台线程释放内存也要时间,如果同时有多个大 Key UNLINK,后台线程也会拥塞。
6.2 lazyfree 配置开了是不是就不用担心大 Key 了?
不是。lazyfree 解决的是"删除时不阻塞主线程",但大 Key 的其他危害依然存在:网络传输阻塞(HGETALL 返回 200MB)、Cluster 迁移卡死、AOF 重写卡顿、内存碎片化。lazyfree 是安全网不是免死金牌——大 Key 本身就是设计缺陷,应该从源头拆分,而不是靠 lazyfree 兜底。
6.3 大 Key 是怎么产生的?
四种典型来源:① 日志累积型——用户行为/访问日志全塞一个 Key,没设 TTL(本文案例);② 缓存堆积型——把 DB 查询结果全量缓存,不分页不裁剪;③ 配置型——把省市区树形数据全塞一个 Hash,全国几千条不拆分;④ 数据结构误用——用 List 存活跃用户列表,用户增长到百万级不换数据结构。预防的核心是代码 review 时检查"这个 Key 会增长吗?有上限吗?有 TTL 吗?"。
6.4 Cluster 模式下大 Key 迁移怎么办?
Cluster 的 slot 迁移用 MIGRATE 命令搬运 Key——大 Key 迁移时 MIGRATE 会阻塞源节点和目标节点(同步传输整个 Value)。200MB 的 Key 迁移可能阻塞 5~10 秒。解法:① 迁移前先拆分大 Key(按 field 分到多个小 Key);② Redis 7.0+ 的 MIGRATE 支持 COPY + REPLACE 异步模式(部分场景缓解);③ 扩缩容时避开高峰期。生产纪律:扩容前先跑大 Key 扫描,大 Key 不迁移只拆分。
6.5 HSCAN 会阻塞主线程吗?
不会。HSCAN(以及 SCAN/SSCAN/ZSCAN)是增量式遍历——每次只取 cursor 指定位置的一批元素,O(1)~O(N) 取决于 COUNT 参数。但要注意:① HSCAN 的 COUNT 只是建议值不是硬限——可能返回比 COUNT 多的元素;② 如果 Hash 在 SCAN 过程中被修改(field 增删),可能出现重复或遗漏(弱一致性)。HSCAN 是扫描大 Hash 的安全方式,但不能保证遍历完整性。
6.6 如何在代码层防止写入大 Key?
在 SET/HSET 前做大小检查——用 AOP 或 RedisTemplate 拦截器,对写入的 Value 序列化后检查字节数,超限抛异常拒绝写入。更有效的做法是在设计阶段就拆分:用户行为按天分 Key,订单缓存按订单号分 Key,省市区树按层级分 Key。设计阶段的拆分比运行时的拦截更治本——运行时拦截只能兜底,不能解决已有的大 Key。
七、总结
大 Key 治理速查卡
┌────────────┬──────────────────────────────────────────┐
│ 阶段 │ 关键动作 │
├────────────┼──────────────────────────────────────────┤
│ 发现 │ --bigkeys 日常巡检(采样) │
│ │ scan-bigkeys.sh 定期全量扫描 │
│ │ MEMORY USAGE 精确查体积 │
│ │ SLOWLOG 确认是否阻塞过主线程 │
├────────────┼──────────────────────────────────────────┤
│ 止血 │ UNLINK 替代 DEL(异步删除) │
│ │ 超大 Key 用 HSCAN+HDEL 分批删 │
│ │ 开 lazyfree-* 全局异步删除 │
├────────────┼──────────────────────────────────────────┤
│ 修复 │ 大 Key 拆分(按时间/用户/ID 分片) │
│ │ 所有 Key 必设 TTL │
│ │ 禁止大结果集操作(HGETALL 0 -1) │
├────────────┼──────────────────────────────────────────┤
│ 预防 │ 代码规范:单 Value ≤10KB / Hash ≤10000 field│
│ │ 定时扫描 Cron + 钉钉告警 │
│ │ 扩容前先查大 Key │
└────────────┴──────────────────────────────────────────┘
关键数据
| 指标 | 事故时 | 修复后 |
|---|---|---|
| 大 Key 体积 | 200MB | < 1MB |
| DEL 阻塞 | 3s | < 1ms(UNLINK) |
| P99 延迟 | 800ms → 30s | 2ms |
| 无 TTL Key | 有 | 全部设 TTL |
| 告警数 | 47 条 | 0 |
一句话
Redis 是单线程——一个 200MB 的 Hash 用 DEL 删除阻塞主线程 3 秒,3 秒内所有命令排队,集群雪崩。根因不是 Redis 性能不行,是业务把用户行为日志全塞进一个 Hash 没设过期时间。修复的紧急动作是 UNLINK 替代 DEL(异步删除不阻塞主线程),根治方案是按时间分片拆分大 Key + 全局必设 TTL。但大 Key 的本质是设计缺陷——单 Value > 10KB / Hash > 10000 field 就是设计有问题,不是运行时该靠 lazyfree 兜底的。治理三步走:--bigkeys 巡检发现、UNLINK+分批删除止血、按维度拆分+TTL 根治,加定时扫描 Cron + 钉钉告警做最后一道防线。
给团队的建议
| 项 | 建议 |
|---|---|
| 紧急 | 大 Key 用 UNLINK 删除,禁用 DEL 删大 Key |
| 配置 | 开启 lazyfree-* 五项全局异步删除 |
| 拆分 | 日志型数据按时间分 Key,缓存型数据按 ID 分 Key |
| TTL | 所有 Key 必设过期时间,Code Review 检查 |
| 扫描 | 每日 Cron 全量扫描 > 10MB 的 Key + 钉钉告警 |
| 扩容 | 扩容前先跑大 Key 扫描,大 Key 先拆分再迁移 |
| 规范 | 禁止 HGETALL/LRANGE 0 -1/SMEMBERS 在大集合上 |
互动话题:你们 Redis 里有没有定时扫描大 Key?有没有踩过 DEL 大 Key 导致阻塞的坑?评论区聊聊。
参考资料
- Redis 官方文档:DEL vs UNLINK
- Redis 官方文档:--bigkeys
- Redis 官方文档:lazyfree 配置
- Redis MEMORY USAGE 命令
- Redis SCAN 命令族
- Redis Cluster MIGRATE 与大 Key
- redis_exporter 监控指标
标题:Redis 大 Key 阻塞排查:一个 200MB 的 Hash 拖垮了整个集群
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/17/1789223612536.html
公众号:服务端技术精选
- 引言
- 一、为什么一个大 Key 能拖垮整个集群
- 1.1 Redis 单线程模型
- 1.2 大 Key 的危害链
- 1.3 什么样的 Key 算"大 Key"
- 二、排查过程
- 2.1 第一步:确认是不是大 Key 引起的
- 2.2 --bigkeys 原理与局限
- 2.3 第二步:确认 DEL 阻塞
- 2.4 第三步:定位根因
- 2.5 事故时间线
- 三、修复方案
- 3.1 紧急止血:UNLINK 替代 DEL
- 3.2 超大 Key 分批删除脚本
- 3.3 根因修复:大 Key 拆分(Hash 按时间分片)
- 3.4 拆分前后对比
- 四、预防机制
- 4.1 全局配置:异步删除
- 4.2 客户端规范:代码层防大 Key
- 4.3 监控告警
- 五、大 Key 扫描脚本
- 5.1 全量扫描脚本
- 5.2 扫描输出示例
- 5.3 定时扫描 Cron
- 六、常见问题
- 6.1 UNLINK 和 DEL 有什么区别?
- 6.2 lazyfree 配置开了是不是就不用担心大 Key 了?
- 6.3 大 Key 是怎么产生的?
- 6.4 Cluster 模式下大 Key 迁移怎么办?
- 6.5 HSCAN 会阻塞主线程吗?
- 6.6 如何在代码层防止写入大 Key?
- 七、总结
- 大 Key 治理速查卡
- 关键数据
- 一句话
- 给团队的建议
- 参考资料
评论