第 38 章
慢查询与大 Key 分析
第38章 慢查询与大 Key 分析
性能问题的根源往往藏在细节中:一个被遗忘的 KEYS *、一个膨胀到十万 field 的 Hash、一个被高频访问的热 key。本章系统讲解如何发现、定位、分析并修复这些问题。
1. slowlog 配置与使用
1.1 配置参数
# redis.conf
slowlog-log-slower-than 10000 # 单位:微秒(μs)。10000μs = 10ms
# 设为 0 记录所有命令(调试用)
# 设为 -1 禁用 slowlog
slowlog-max-len 128 # 最多保留128条(环形缓冲,超出丢弃旧的)
动态修改(无需重启):
CONFIG SET slowlog-log-slower-than 5000 # 改为 5ms
CONFIG SET slowlog-max-len 256
CONFIG REWRITE # 持久化到 redis.conf
1.2 查询慢日志
SLOWLOG GET # 获取所有(最多 slowlog-max-len 条)
SLOWLOG GET 10 # 最近 10 条
SLOWLOG LEN # 当前慢日志数量
SLOWLOG RESET # 清空慢日志
# 输出格式(每条包含4个字段):
# 1) 1) (integer) 14 ← 日志 ID(自增)
# 2) (integer) 1699000000 ← Unix 时间戳(命令执行时刻)
# 3) (integer) 28000 ← 执行耗时(微秒),28000μs = 28ms
# 4) 1) "KEYS" ← 命令名
# 2) "*" ← 参数(默认截断到128字节)
# 5) "127.0.0.1:54321" ← 客户端地址(Redis 4.0+)
# 6) "myapp" ← 客户端名称(CLIENT SETNAME 设置)
1.3 Python 解析慢日志
import redis
from datetime import datetime
r = redis.Redis()
def analyze_slowlog(count: int = 50):
entries = r.slowlog_get(count)
for entry in entries:
ts = datetime.fromtimestamp(entry['start_time'])
duration_ms = entry['duration'] / 1000
cmd = ' '.join(
arg.decode() if isinstance(arg, bytes) else str(arg)
for arg in entry['command']
)
print(f"[{ts}] {duration_ms:.1f}ms | {cmd[:120]}")
analyze_slowlog()
2. 常见慢命令一览
| 命令 | 复杂度 | 阻塞风险 | 替代方案 |
|---|---|---|---|
KEYS * |
O(N) | 极高 | SCAN |
HGETALL |
O(N) | 高(field多时) | HSCAN 分批 |
SMEMBERS |
O(N) | 高(成员多时) | SSCAN 分批 |
SORT |
O(N+M·log M) | 高 | 预排序存 ZSET |
LRANGE 0 -1 |
O(N) | 高(列表长时) | LRANGE 分页 |
SUNIONSTORE |
O(N) | 高 | 拆分 + 归并 |
SINTERSTORE |
O(N·M) | 极高 | 分步操作 |
ZRANGEBYLEX |
O(log N+M) | 低 | 可用 |
DEL(大key) |
O(N) | 高 | UNLINK(异步删除) |
FLUSHDB |
O(N) | 极高(同步) | FLUSHDB ASYNC |
2.1 UNLINK:异步删除大 Key
# DEL 是同步删除,大 key 会阻塞主线程
DEL big_hash # 危险:10万field的Hash,DEL耗时数百ms
# UNLINK 主线程只做脱离引用(O(1)),后台线程异步回收内存
UNLINK big_hash # 安全
# 代码层面:对可能的大 key 统一使用 UNLINK
def safe_delete(*keys):
return r.unlink(*keys)
3. bigkeys 扫描
3.1 使用 redis-cli --bigkeys
redis-cli --bigkeys
# 使用 SCAN 遍历,不阻塞主线程
# 对每种类型分别统计最大的 key
# 加 -i 参数降低扫描频率(减少对生产的影响)
redis-cli --bigkeys -i 0.1 # 每执行 100 条 SCAN 休眠 0.1 秒
输出示例:
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest string found so far 'config:blob' with 512000 bytes
[23.45%] Biggest hash found so far 'user:profile:9999' with 18432 fields
[67.89%] Biggest zset found so far 'leaderboard:all' with 2048321 members
-------- summary -------
Sampled 1000000 keys in the keyspace!
Total key length in bytes is 24000000 (avg len 24.00)
Biggest string found 'config:blob' has 512000 bytes
Biggest hash found 'user:profile:9999' has 18432 fields
Biggest zset found 'leaderboard:all' has 2048321 members
Biggest set found 'tags:popular' has 10240 members
Biggest list found 'queue:jobs' has 50000 items
1000000 strings with 8000000 bytes (08.00 bytes mean)
50000 hashs with 920000 fields (18.40 fields mean)
...
3.2 单 key 内存分析
# 递归计算 key 占用的所有内存(包括编码开销)
MEMORY USAGE user:profile:9999
# 返回:字节数
MEMORY USAGE user:profile:9999 SAMPLES 0 # 不采样,精确计算(慢)
4. hotkeys 扫描
4.1 开启 LFU 策略
hotkeys 扫描需要 Redis 使用 LFU 淘汰策略:
# redis.conf
maxmemory-policy allkeys-lfu # 或 volatile-lfu
# 动态设置
CONFIG SET maxmemory-policy allkeys-lfu
redis-cli --hotkeys
# 扫描输出:
# [67.89%] Hot key 'product:detail:10086' found so far with counter 9876
# [89.12%] Hot key 'user:session:abc123' found so far with counter 7654
#
# -------- summary -------
# Sampled 1000000 keys in the keyspace!
# hot key found with counter: 9876 keyname: product:detail:10086
# hot key found with counter: 7654 keyname: user:session:abc123
4.2 实时监控热 key(MONITOR 方式)
# 危险:MONITOR 会输出所有命令,大流量下严重影响性能
# 仅用于短时间紧急排查
redis-cli monitor | head -n 10000 | grep -oP "(?<=\")[^\"]+(?=\")" | sort | uniq -c | sort -rn | head -20
5. 内存分析工具
5.1 在线工具
# 单 key 内存
redis-cli memory usage mykey
# 内存使用统计
redis-cli memory stats
# 输出:peak.allocated / total.allocated / fragmentation.ratio 等
# 内存医生(建议)
redis-cli memory doctor
# 示例输出:
# Hi Sam, I can't find any memory issues in your instance. I can however spot
# a few issues in your configuration:
# * Your vm.overcommit_memory is set to 0, ...
5.2 离线 RDB 分析
# 方式1:redis-rdb-tools(Python)
pip install rdbtools python-lzf
rdb --command memory dump.rdb > memory.csv
# 输出 CSV:database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
# 排序找最大 key
sort -t',' -k4 -rn memory.csv | head -20
# 方式2:rdb-cli(Go,更快)
go install github.com/HDT3213/rdb@latest
rdb -c memory -port 7379 dump.rdb
# 启动 HTTP 服务,浏览器可视化分析
5.3 生成 RDB 快照
# 手动触发 RDB(会 fork,对性能有短暂影响)
BGSAVE
LASTSAVE # 查看最近一次 RDB 时间戳
# 等待完成
redis-cli info persistence | grep rdb_bgsave_in_progress
6. 大 Key 拆分实战
6.1 场景:大 Hash(10 万 field)
# 问题:HGETALL user:1000 返回 100000 个 field,耗时 500ms+
HGETALL user:1000
拆分方案:按 field hash 分桶
import hashlib
SHARD_COUNT = 16
def hash_shard(field: str) -> int:
return int(hashlib.md5(field.encode()).hexdigest(), 16) % SHARD_COUNT
def hset_sharded(base_key: str, field: str, value: str):
shard = hash_shard(field)
r.hset(f"{base_key}:s{shard}", field, value)
def hget_sharded(base_key: str, field: str) -> str:
shard = hash_shard(field)
return r.hget(f"{base_key}:s{shard}", field)
def hgetall_sharded(base_key: str) -> dict:
result = {}
pipe = r.pipeline()
for i in range(SHARD_COUNT):
pipe.hgetall(f"{base_key}:s{i}")
shards = pipe.execute()
for shard_data in shards:
result.update({k.decode(): v.decode() for k, v in shard_data.items()})
return result
6.2 场景:大 Set(百万成员)
SET_SHARD_COUNT = 100
def sadd_sharded(base_key: str, member: str):
shard = hash(member) % SET_SHARD_COUNT
r.sadd(f"{base_key}:s{shard}", member)
def sismember_sharded(base_key: str, member: str) -> bool:
shard = hash(member) % SET_SHARD_COUNT
return bool(r.sismember(f"{base_key}:s{shard}", member))
def scard_sharded(base_key: str) -> int:
pipe = r.pipeline()
for i in range(SET_SHARD_COUNT):
pipe.scard(f"{base_key}:s{i}")
return sum(pipe.execute())
6.3 场景:大 ZSet(亿级成员排行榜)
见第36章"超大排行榜"章节,此处不赘述。
6.4 大 String(值超过 1MB)
# 问题:存储了 Blob 或序列化后的大对象
SET product:all "[{...10万条商品JSON...}]" # 10MB
# 解决方案1:压缩后存储(客户端压缩)
# 解决方案2:拆分为多个 key(结合分页)
# 解决方案3:大对象存 OSS/S3,Redis 只存 URL + 元数据
import gzip
import json
def set_compressed(key: str, data: dict, ttl: int = 3600):
"""压缩后存储大 JSON"""
raw = json.dumps(data, ensure_ascii=False).encode()
compressed = gzip.compress(raw)
r.setex(key, ttl, compressed)
def get_compressed(key: str) -> dict:
compressed = r.get(key)
if not compressed:
return None
raw = gzip.decompress(compressed)
return json.loads(raw.decode())
7. 内存碎片率处理
INFO memory | grep mem_fragmentation_ratio
# mem_fragmentation_ratio: 1.89 ← 碎片率 = RSS / used_memory
# 正常范围:1.0–1.5
# > 1.5:碎片严重,考虑整理
# < 1.0:内存被 swap 或使用 jemalloc 统计偏差
7.1 主动碎片整理(Redis 4.0+)
# 开启自动碎片整理(对性能有影响,建议在低峰期开启)
CONFIG SET activedefrag yes
CONFIG SET active-defrag-ignore-bytes 100mb # 碎片超过100MB才整理
CONFIG SET active-defrag-enabled yes
CONFIG SET active-defrag-threshold-lower 10 # 碎片率>10%时开始
CONFIG SET active-defrag-threshold-upper 100 # 碎片率>100%时全速整理
CONFIG SET active-defrag-cycle-min 1 # 最低CPU占用%
CONFIG SET active-defrag-cycle-max 25 # 最高CPU占用%
# 手动触发一次整理(Redis 4.0+)
MEMORY PURGE
7.2 重启整理(终极手段)
大碎片率的最彻底解决:主从切换 + 重启旧主。RDB 加载后内存紧凑,碎片消除。
8. 生产排查 Checklist
发现 Redis 响应变慢时的标准排查流程:
□ 1. 查看慢日志:SLOWLOG GET 20
□ 2. 确认 QPS:INFO stats | grep instantaneous_ops_per_sec
□ 3. 确认内存:INFO memory(used_memory / maxmemory / fragmentation_ratio)
□ 4. 确认连接数:INFO clients(connected_clients / blocked_clients)
□ 5. 有无大 key:redis-cli --bigkeys -i 0.1(低流量时执行)
□ 6. 有无热 key:redis-cli --hotkeys(需 LFU 策略)
□ 7. 查看持久化状态:INFO persistence(aof_rewrite / rdb_bgsave 是否在进行)
□ 8. 查看复制状态:INFO replication(repl_backlog 是否溢出)
□ 9. 检查 CPU:top -p $(pgrep redis-server)
□ 10. 检查网络:netstat -an | grep 6379 | wc -l
本章总结
- slowlog 是定位慢命令的第一手段,生产建议阈值 5–10ms
KEYS *必须从代码和 DBA 操作中彻底禁止- 大 key 删除使用
UNLINK,避免主线程阻塞 redis-cli --bigkeys和--hotkeys是常规巡检工具,建议每周执行- 大 Hash/Set/ZSet 拆分:按 field/member hash 分桶,Pipeline 并行读取
- 内存碎片率 > 1.5 时开启
activedefrag或安排低峰重启