适用场景
这篇文章适用于线上 Redis 作为缓存、会话、计数器或排行榜组件时,出现接口响应时间突然升高、Redis CPU 飙升、慢查询增多、网络出口流量变大等问题的排查。
典型环境包括:
- 单机 Redis 或 Redis Sentinel 架构;
- Redis Cluster 分片集群;
- 业务使用 Python、Java、Go 等客户端访问 Redis;
- 缓存中存在 hash、list、set、zset、string 等多种数据结构;
- 应用侧偶发出现超时,但 Redis 进程没有直接宕机。
本文重点不是解释 Redis 基础命令,而是给出一个可以在线上执行的排查顺序:先确认延迟,再定位慢命令,再识别大 key 和热 key,最后给出拆分、限流、过期时间和客户端改造方案。
现象描述
一次常见故障可能是这样的:
- 应用接口 P95、P99 延迟从几十毫秒升到几百毫秒甚至数秒;
- Redis 所在机器 CPU 使用率较高,单核接近打满;
- 应用日志里出现
Read timed out、Connection reset、Command timed out; - Redis 慢日志里出现
HGETALL、LRANGE 0 -1、SMEMBERS、ZRANGE、GET等命令; - 监控中 Redis
instantaneous_ops_per_sec不一定明显变高,但网络入/出流量明显上升; - 删除某些缓存 key 或重启应用后短暂恢复,过一段时间又复现。
这类问题经常不是 Redis “不能用”,而是少数 key 的访问方式破坏了 Redis 单线程执行模型的稳定性。
可能原因
Redis 大 key 和热 key 经常同时出现,但它们不是一个概念。
大 key 指单个 key 占用的数据量过大,常见形式如下:
- string value 很大,例如一个 key 存了几 MB 的 JSON;
- hash field 数量很多,例如一个用户画像 hash 存了几十万个 field;
- list、set、zset 元素数量过多;
- 一个 key 的序列化对象持续追加,长期没有拆分或清理。
热 key 指某个 key 被高频访问,常见形式如下:
- 首页配置、热门商品、全站开关等 key 被所有请求读取;
- 某个活动、秒杀、热点新闻导致单个 key 访问量突然升高;
- 缓存击穿后大量请求同时访问同一个 key;
- 业务代码循环内重复访问同一个 key。
大 key 的直接问题是单次命令执行时间长、网络传输大、内存回收慢;热 key 的直接问题是访问集中,导致单个 Redis 节点负载不均。二者叠加时,问题会非常明显:一个很大的 key 又被频繁读取,Redis 主线程会被连续占用,其他正常请求也会排队。
排查思路总览
建议按照下面的顺序排查:
- 确认 Redis 延迟是否真实升高;
- 查看慢日志,确认是否存在危险命令;
- 查看 Redis 实时状态,判断 CPU、连接、内存、网络是否异常;
- 使用安全方式扫描大 key;
- 从应用日志、监控或 Redis 命令统计中识别热 key;
- 对命中的 key 做结构分析;
- 制定拆分、缓存副本、限流、过期和代码改造方案。
不要一上来就在生产环境执行 KEYS *、HGETALL、SMEMBERS 这类全量命令。它们可能让本来已经变慢的 Redis 更慢。
第一步:确认 Redis 延迟
先在 Redis 机器或同网段机器上执行延迟测试:
redis-cli -h 127.0.0.1 -p 6379 --latency
输出示例:
min: 0, max: 153, avg: 3.21 (1280 samples)
关键字段:
min:最小延迟;max:采样期间最大延迟;avg:平均延迟。
如果 max 偶尔冲到几十毫秒甚至几百毫秒,就要继续看慢命令和系统资源。
也可以执行:
redis-cli -h 127.0.0.1 -p 6379 --latency-history
这个命令会分段输出延迟,更容易观察问题是否持续存在。
第二步:查看慢日志
查看慢日志配置:
redis-cli CONFIG GET slowlog-log-slower-than
redis-cli CONFIG GET slowlog-max-len
slowlog-log-slower-than 的单位是微秒。比如 10000 表示超过 10ms 的命令会进入慢日志。
查看最近慢日志:
redis-cli SLOWLOG GET 20
示例输出中需要重点关注命令部分:
1) 1) (integer) 128
2) (integer) 1783439201
3) (integer) 62435
4) 1) "HGETALL"
2) "user:profile:all"
5) "10.0.12.33:53128"
6) "app-api"
关键字段:
- 第 3 个字段是执行耗时,单位微秒;
- 命令是
HGETALL user:profile:all; - 客户端地址是
10.0.12.33:53128; - 客户端名称是
app-api。
如果慢日志里反复出现下面这些命令,要特别警惕:
HGETALL large_hash_key
LRANGE large_list_key 0 -1
SMEMBERS large_set_key
ZRANGE large_zset_key 0 -1 WITHSCORES
GET huge_json_string
DEL very_large_key
这些命令不是绝对不能用,而是不能在大集合或大 value 上高频使用。
第三步:查看 Redis 实时状态
执行:
redis-cli INFO stats
redis-cli INFO commandstats
redis-cli INFO memory
redis-cli INFO clients
重点看这些字段:
instantaneous_ops_per_sec:4200
instantaneous_input_kbps:512.34
instantaneous_output_kbps:18342.80
rejected_connections:0
expired_keys:123456
evicted_keys:0
used_memory_human:6.20G
mem_fragmentation_ratio:1.48
connected_clients:380
blocked_clients:0
判断方法:
instantaneous_output_kbps很高,说明 Redis 正在向客户端返回大量数据,常见于读取大 key;used_memory_human持续上涨,说明缓存没有及时淘汰或 key 设计不合理;connected_clients异常升高,可能是应用连接池配置不合理;evicted_keys增长,说明内存已经触发淘汰策略;blocked_clients大于 0,要检查是否使用了阻塞命令。
查看命令统计:
redis-cli INFO commandstats | sort
输出示例:
cmdstat_hgetall:calls=8231,usec=91234567,usec_per_call=11084.21,rejected_calls=0,failed_calls=0
cmdstat_get:calls=982312,usec=3182011,usec_per_call=3.23,rejected_calls=0,failed_calls=0
cmdstat_zrange:calls=13822,usec=42301822,usec_per_call=3060.47,rejected_calls=0,failed_calls=0
关键字段:
calls:命令调用次数;usec:累计耗时;usec_per_call:平均每次命令耗时。
如果某个命令 calls 不算高,但 usec_per_call 很高,通常说明它操作的数据量大。
第四步:安全扫描大 key
生产环境不要用 KEYS *。可以使用 Redis 自带的 --bigkeys:
redis-cli -h 127.0.0.1 -p 6379 --bigkeys
它会基于 SCAN 增量扫描,风险比 KEYS 小很多。输出示例:
Biggest string found '"cache:home:payload"' has 7340032 bytes
Biggest hash found '"user:profile:all"' has 820000 fields
Biggest list found '"queue:retry:failed"' has 430000 items
Biggest zset found '"rank:daily:20260708"' has 280000 members
如果担心扫描对线上造成压力,可以降低扫描频率:
redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1
-i 0.1 表示每 100 次 SCAN 休眠 0.1 秒。扫描会变慢,但对线上更友好。
对于 Redis 4.0 及以上版本,可以用 MEMORY USAGE 查看单个 key 的估算内存:
redis-cli MEMORY USAGE cache:home:payload
redis-cli MEMORY USAGE user:profile:all
再结合类型和长度查看:
redis-cli TYPE user:profile:all
redis-cli HLEN user:profile:all
redis-cli STRLEN cache:home:payload
redis-cli LLEN queue:retry:failed
redis-cli ZCARD rank:daily:20260708
这些命令只返回长度或估算值,通常比拉取完整数据安全。
第五步:识别热 key
热 key 的定位方式和架构有关。
如果是 Redis 4.0 以上,可以使用 LFU 相关信息辅助判断,但前提是内存淘汰策略启用了 LFU,例如 allkeys-lfu 或 volatile-lfu。查看配置:
redis-cli CONFIG GET maxmemory-policy
如果策略支持 LFU,可以查看对象频率:
redis-cli OBJECT FREQ cache:home:payload
返回值越高,说明近期访问越频繁。注意这个值是近似统计,不是精确 QPS。
如果没有 LFU,可以从应用侧入手:
grep "redis timeout" /var/log/app/app.log | awk '{print $1,$2,$NF}' | sort | uniq -c | sort -nr | head
如果应用日志里记录了 key,可以统计高频 key:
grep "redis key=" /var/log/app/app.log \
| sed -n 's/.*key=\([^ ]*\).*/\1/p' \
| sort | uniq -c | sort -nr | head -20
也可以临时观察 Redis 正在执行的命令:
redis-cli MONITOR
但 MONITOR 会输出所有命令,本身有性能开销。线上只建议短时间使用,例如 10 到 30 秒,并尽量在低峰期执行:
timeout 10 redis-cli MONITOR | awk '{print $NF}' | sort | uniq -c | sort -nr | head
如果系统没有 timeout 命令,可以手动中断,或在测试环境复现。
Redis Cluster 可以结合节点维度监控判断热点分片:
redis-cli -c -h redis-cluster.example.com -p 6379 CLUSTER NODES
redis-cli -c -h redis-cluster.example.com -p 6379 INFO stats
如果只有某个节点 CPU、网络、延迟明显高于其他节点,通常说明热点 key 落在该节点。
定位示例:HGETALL 大 hash 拖慢接口
假设慢日志里反复出现:
HGETALL user:profile:all
先确认 key 类型和长度:
redis-cli TYPE user:profile:all
redis-cli HLEN user:profile:all
redis-cli MEMORY USAGE user:profile:all
输出:
hash
820000
168435120
这说明 user:profile:all 是一个 hash,有 82 万个 field,估算占用约 160MB。此时继续执行 HGETALL 会把所有 field 和 value 一次性返回给客户端,Redis 要编码响应,网络要传输大量数据,客户端还要反序列化,整个链路都会慢。
再看应用代码,可能有类似逻辑:
profiles = redis_client.hgetall("user:profile:all")
return profiles.get(str(user_id))
这段代码为了拿一个用户资料,却每次拉取全量 hash。正确方式应该改成按 field 读取:
profile = redis_client.hget("user:profile:all", str(user_id))
return profile
如果单个 hash 本身仍然过大,可以按用户 ID 取模拆分:
bucket = user_id % 128
key = f"user:profile:{bucket}"
profile = redis_client.hget(key, str(user_id))
这样可以把一个超大 hash 拆成 128 个较小 hash,降低单 key 内存和迁移风险。
定位示例:热门配置 key 被全站请求打爆
假设监控显示 Redis QPS 不高,但某个节点 CPU 接近 100%,应用日志统计发现 config:global 出现频率非常高:
grep "config:global" /var/log/app/app.log | wc -l
如果这是全站每个请求都读取的配置,就不应该每次都打 Redis。可以在应用进程内增加短 TTL 本地缓存:
import time
_local_cache = {}
def get_global_config(redis_client):
now = time.time()
item = _local_cache.get("config:global")
if item and item["expire_at"] > now:
return item["value"]
value = redis_client.get("config:global")
_local_cache["config:global"] = {
"value": value,
"expire_at": now + 5,
}
return value
这段代码把 Redis 读取压到每个应用进程最多 5 秒一次。配置变化允许几秒延迟时,这是非常有效的降压方式。
如果配置必须更实时,可以使用版本号 key:
config:global:version
config:global:data
应用只高频读取小的版本号,版本变化后再读取完整配置。
修复方案
1. 禁止全量读取大集合
把这些命令从核心链路中移除:
HGETALL
SMEMBERS
LRANGE key 0 -1
ZRANGE key 0 -1
替代方式:
HGET key field
HMGET key field1 field2 field3
SSCAN key cursor COUNT 100
HSCAN key cursor COUNT 100
ZRANGE key 0 99 WITHSCORES
核心原则是分页、按需读取、限制返回数量。
2. 拆分大 key
常用拆分方式:
- 按业务 ID 取模:
user:profile:{bucket}; - 按时间拆分:
rank:daily:20260708、rank:daily:20260709; - 按租户拆分:
tenant:{tenant_id}:config; - 按冷热拆分:近期数据放 Redis,历史数据落数据库或对象存储。
示例:
def profile_key(user_id: int) -> str:
return f"user:profile:{user_id % 128}"
拆分后要注意数据迁移过程。建议先双写新旧 key,再灰度读新 key,确认稳定后清理旧 key。
3. 删除大 key 使用异步方式
直接 DEL large_key 可能阻塞 Redis 主线程。Redis 4.0 以后可以优先使用:
redis-cli UNLINK user:profile:all
UNLINK 会把释放内存的动作交给后台线程,降低阻塞风险。
如果版本较老,只能 DEL,建议在低峰期分批清理,或者通过业务脚本逐步删除集合元素。
4. 热 key 增加本地缓存或副本 key
对允许短暂不一致的数据,可以增加应用本地缓存:
local_ttl_seconds = 3
对读多写少的热点数据,可以构造多个副本 key:
hot:config:0
hot:config:1
hot:config:2
hot:config:3
客户端读取时随机选择一个:
import random
key = f"hot:config:{random.randint(0, 3)}"
value = redis_client.get(key)
更新时同时更新所有副本。这样可以在 Redis Cluster 中把读流量打散到不同槽位和节点,但要接受更新逻辑更复杂的问题。
5. 防止缓存击穿
热点 key 过期瞬间,可能大量请求同时打到数据库并回写 Redis。可以使用互斥锁:
def get_with_mutex(redis_client, key, build_func, ttl=300):
value = redis_client.get(key)
if value is not None:
return value
lock_key = f"lock:{key}"
got_lock = redis_client.set(lock_key, "1", nx=True, ex=10)
if got_lock:
try:
value = build_func()
redis_client.set(key, value, ex=ttl)
return value
finally:
redis_client.delete(lock_key)
time.sleep(0.05)
return redis_client.get(key)
生产环境中还要考虑锁误删、重试次数、降级返回旧值等细节。
6. 给 key 设置合理过期时间
检查没有过期时间的业务缓存:
redis-cli TTL cache:home:payload
返回值含义:
-1:key 存在,但没有过期时间;-2:key 不存在;- 大于 0:剩余过期秒数。
对缓存类数据,尽量设置 TTL,并增加随机抖动,避免大量 key 同时过期:
import random
ttl = 3600 + random.randint(0, 300)
redis_client.set(cache_key, value, ex=ttl)
预防措施
建议把下面几项加入上线规范:
- 禁止在核心链路使用
KEYS、HGETALL、SMEMBERS、无边界LRANGE、无边界ZRANGE; - 代码评审时检查 Redis 命令是否有分页和数量限制;
- 对可能膨胀的 key 设计拆分规则;
- 所有缓存 key 默认设置 TTL,除非明确是持久状态;
- 监控 Redis 慢日志数量、单节点 CPU、网络输出、内存使用、连接数;
- 定期在低峰期执行
--bigkeys -i 0.1,保存扫描结果做趋势对比; - 对首页配置、热门活动、排行榜等热点 key 设计本地缓存或副本 key;
- 删除大 key 优先使用
UNLINK; - Redis Cluster 要关注单节点负载差异,不能只看集群总 QPS。
应急处理清单
线上已经出现明显超时时,可以按下面顺序处理:
- 用
SLOWLOG GET 20找到最可疑命令; - 用
TYPE、HLEN、LLEN、SCARD、ZCARD、STRLEN确认 key 规模; - 临时下线或限流调用大 key 的接口;
- 如果是热点配置 key,快速增加应用本地缓存;
- 如果是大 key 删除,优先使用
UNLINK; - 如果是缓存击穿,临时延长 TTL 或增加互斥构建;
- 故障恢复后做数据结构拆分和代码改造。
总结
Redis 大 key 和热 key 问题的本质,是少数 key 让 Redis 的单线程执行、网络传输和客户端反序列化都变重。排查时不要只看 Redis 是否存活,也不要只看总 QPS。更有效的路径是:用延迟命令确认问题,用慢日志找到危险命令,用 INFO commandstats 看命令成本,用 --bigkeys 和长度命令确认 key 规模,再结合应用日志判断是否存在热点访问。
修复时优先改访问方式:全量读取改成按需读取,大集合改成分片存储,热点读取加本地缓存或副本 key,大 key 删除使用 UNLINK。只要在设计阶段控制 key 的规模和访问频率,Redis 就能稳定地承担高并发缓存压力。
Discussion
评论