前言
💡 痛点 :Redis 突然报错
OOM command not allowed when used memory > 'maxmemory'?服务开始报错,缓存数据丢失?不知道是哪个 key 占用了大量内存?🎯 解决方案 :掌握 Redis 内存排查与恢复 --- 从内存监控、key 分析、到问题定位与解决。
Redis 内存问题排查流程图:
#mermaid-svg-oD2IGacKk5BT4gMV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oD2IGacKk5BT4gMV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oD2IGacKk5BT4gMV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oD2IGacKk5BT4gMV .error-icon{fill:#552222;}#mermaid-svg-oD2IGacKk5BT4gMV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oD2IGacKk5BT4gMV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oD2IGacKk5BT4gMV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oD2IGacKk5BT4gMV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oD2IGacKk5BT4gMV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oD2IGacKk5BT4gMV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oD2IGacKk5BT4gMV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oD2IGacKk5BT4gMV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oD2IGacKk5BT4gMV .marker.cross{stroke:#333333;}#mermaid-svg-oD2IGacKk5BT4gMV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oD2IGacKk5BT4gMV p{margin:0;}#mermaid-svg-oD2IGacKk5BT4gMV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oD2IGacKk5BT4gMV .cluster-label text{fill:#333;}#mermaid-svg-oD2IGacKk5BT4gMV .cluster-label span{color:#333;}#mermaid-svg-oD2IGacKk5BT4gMV .cluster-label span p{background-color:transparent;}#mermaid-svg-oD2IGacKk5BT4gMV .label text,#mermaid-svg-oD2IGacKk5BT4gMV span{fill:#333;color:#333;}#mermaid-svg-oD2IGacKk5BT4gMV .node rect,#mermaid-svg-oD2IGacKk5BT4gMV .node circle,#mermaid-svg-oD2IGacKk5BT4gMV .node ellipse,#mermaid-svg-oD2IGacKk5BT4gMV .node polygon,#mermaid-svg-oD2IGacKk5BT4gMV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oD2IGacKk5BT4gMV .rough-node .label text,#mermaid-svg-oD2IGacKk5BT4gMV .node .label text,#mermaid-svg-oD2IGacKk5BT4gMV .image-shape .label,#mermaid-svg-oD2IGacKk5BT4gMV .icon-shape .label{text-anchor:middle;}#mermaid-svg-oD2IGacKk5BT4gMV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oD2IGacKk5BT4gMV .rough-node .label,#mermaid-svg-oD2IGacKk5BT4gMV .node .label,#mermaid-svg-oD2IGacKk5BT4gMV .image-shape .label,#mermaid-svg-oD2IGacKk5BT4gMV .icon-shape .label{text-align:center;}#mermaid-svg-oD2IGacKk5BT4gMV .node.clickable{cursor:pointer;}#mermaid-svg-oD2IGacKk5BT4gMV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oD2IGacKk5BT4gMV .arrowheadPath{fill:#333333;}#mermaid-svg-oD2IGacKk5BT4gMV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oD2IGacKk5BT4gMV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oD2IGacKk5BT4gMV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oD2IGacKk5BT4gMV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oD2IGacKk5BT4gMV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oD2IGacKk5BT4gMV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oD2IGacKk5BT4gMV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oD2IGacKk5BT4gMV .cluster text{fill:#333;}#mermaid-svg-oD2IGacKk5BT4gMV .cluster span{color:#333;}#mermaid-svg-oD2IGacKk5BT4gMV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oD2IGacKk5BT4gMV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oD2IGacKk5BT4gMV rect.text{fill:none;stroke-width:0;}#mermaid-svg-oD2IGacKk5BT4gMV .icon-shape,#mermaid-svg-oD2IGacKk5BT4gMV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oD2IGacKk5BT4gMV .icon-shape p,#mermaid-svg-oD2IGacKk5BT4gMV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oD2IGacKk5BT4gMV .icon-shape .label rect,#mermaid-svg-oD2IGacKk5BT4gMV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oD2IGacKk5BT4gMV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oD2IGacKk5BT4gMV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oD2IGacKk5BT4gMV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} OOM 发生
查看错误日志
检查内存使用
分析大 Key
定位内存占用
检查过期策略
优化内存配置
调整淘汰策略
清理内存
恢复服务
Redis 内存问题类型:
| 类型 | 说明 | 影响 |
|---|---|---|
| 内存溢出 | 使用超过 maxmemory | 命令执行失败 |
| 内存泄漏 | 内存持续增长不释放 | 最终 OOM |
| 大 Key | 单个 Key 占用过大 | 阻塞主线程 |
| Key 堆积 | 过期 key 未及时删除 | 内存持续增长 |
一、Redis 内存机制
1.1 内存使用概览
#mermaid-svg-FuBdtwbGIkfkNkp8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FuBdtwbGIkfkNkp8 .error-icon{fill:#552222;}#mermaid-svg-FuBdtwbGIkfkNkp8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FuBdtwbGIkfkNkp8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .marker.cross{stroke:#333333;}#mermaid-svg-FuBdtwbGIkfkNkp8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FuBdtwbGIkfkNkp8 p{margin:0;}#mermaid-svg-FuBdtwbGIkfkNkp8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .cluster-label text{fill:#333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .cluster-label span{color:#333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .cluster-label span p{background-color:transparent;}#mermaid-svg-FuBdtwbGIkfkNkp8 .label text,#mermaid-svg-FuBdtwbGIkfkNkp8 span{fill:#333;color:#333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .node rect,#mermaid-svg-FuBdtwbGIkfkNkp8 .node circle,#mermaid-svg-FuBdtwbGIkfkNkp8 .node ellipse,#mermaid-svg-FuBdtwbGIkfkNkp8 .node polygon,#mermaid-svg-FuBdtwbGIkfkNkp8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FuBdtwbGIkfkNkp8 .rough-node .label text,#mermaid-svg-FuBdtwbGIkfkNkp8 .node .label text,#mermaid-svg-FuBdtwbGIkfkNkp8 .image-shape .label,#mermaid-svg-FuBdtwbGIkfkNkp8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-FuBdtwbGIkfkNkp8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FuBdtwbGIkfkNkp8 .rough-node .label,#mermaid-svg-FuBdtwbGIkfkNkp8 .node .label,#mermaid-svg-FuBdtwbGIkfkNkp8 .image-shape .label,#mermaid-svg-FuBdtwbGIkfkNkp8 .icon-shape .label{text-align:center;}#mermaid-svg-FuBdtwbGIkfkNkp8 .node.clickable{cursor:pointer;}#mermaid-svg-FuBdtwbGIkfkNkp8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .arrowheadPath{fill:#333333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FuBdtwbGIkfkNkp8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FuBdtwbGIkfkNkp8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FuBdtwbGIkfkNkp8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FuBdtwbGIkfkNkp8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FuBdtwbGIkfkNkp8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FuBdtwbGIkfkNkp8 .cluster text{fill:#333;}#mermaid-svg-FuBdtwbGIkfkNkp8 .cluster span{color:#333;}#mermaid-svg-FuBdtwbGIkfkNkp8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FuBdtwbGIkfkNkp8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FuBdtwbGIkfkNkp8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-FuBdtwbGIkfkNkp8 .icon-shape,#mermaid-svg-FuBdtwbGIkfkNkp8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FuBdtwbGIkfkNkp8 .icon-shape p,#mermaid-svg-FuBdtwbGIkfkNkp8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FuBdtwbGIkfkNkp8 .icon-shape .label rect,#mermaid-svg-FuBdtwbGIkfkNkp8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FuBdtwbGIkfkNkp8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FuBdtwbGIkfkNkp8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FuBdtwbGIkfkNkp8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Redis 内存
数据内存
子进程内存
缓冲内存
管理内存
碎片内存
String 数据
Hash 数据
List 数据
Set 数据
Zset 数据
AOF 重写
RDB 保存
客户端缓冲
AOF 缓冲
bash
# ===== 查看 Redis 内存信息 =====
# 查看内存使用概况
INFO memory
# 关键指标解读
# used_memory: Redis 分配的内存总量(字节)
# used_memory_human: 人类可读格式
# used_memory_rss: Redis 进程实际占用的物理内存
# used_memory_peak: 内存使用峰值
# used_memory_peak_rss: 物理内存峰值
# mem_fragmentation_ratio: 内存碎片率(used_memory_rss / used_memory)
# mem_clients_normal: 普通客户端内存
# mem_clients_slaves: 从节点客户端内存
# mem_aof_buffer: AOF 缓冲区内存
# mem_allocator: 内存分配器(jemalloc)
# 查看详细内存统计
INFO memory | grep -E "used|peak|fragment|clients"
1.2 内存分配策略
bash
# ===== 内存配置 =====
# 查看当前 maxmemory 配置
CONFIG GET maxmemory
# 设置 maxmemory(示例:1GB)
CONFIG SET maxmemory 1073741824
# 设置 maxmemory(永久配置 redis.conf)
# maxmemory 1gb
# maxmemory 10240000 # 单位:字节
# 查看内存淘汰策略
CONFIG GET maxmemory-policy
# 淘汰策略说明:
# - noeviction: 不淘汰,返回错误(默认)
# - volatile-lru: LRU 算法淘汰有过期时间的 key
# - allkeys-lru: LRU 算法淘汰所有 key
# - volatile-lfu: LFU 算法淘汰有过期时间的 key
# - allkeys-lfu: LFU 算法淘汰所有 key
# - volatile-random: 随机淘汰有过期时间的 key
# - allkeys-random: 随机淘汰所有 key
# - volatile-ttl: 淘汰 TTL 最短的 key
# 设置淘汰策略
CONFIG SET maxmemory-policy allkeys-lru
1.3 内存碎片
bash
# ===== 内存碎片处理 =====
# 查看内存碎片率
INFO memory | grep mem_fragmentation_ratio
# mem_fragmentation_ratio: 1.45
# 碎片率说明:
# 1.0 - 1.5: 正常范围
# > 1.5: 内存碎片较多,需要清理
# < 1.0: 使用了 swap
# 手动整理内存碎片(Redis 4.0+)
MEMORY PURGE
# 查看碎片详情
INFO memory
# 内存碎片原因:
# 1. 数据频繁更新(set 操作)
# 2. 大 Key 删除后空间不释放
# 3. 内存分配器(jemalloc)碎片
二、OOM 错误分析
2.1 OOM 错误类型
bash
# ===== OOM 错误类型 =====
# 错误 1: 命令不允许
# OOM command not allowed when used memory > 'maxmemory'
# 原因:内存达到 maxmemory 且淘汰策略为 noeviction
# 错误 2: Lua 脚本内存超限
# BUSY Redis is busy running a script
# 原因:Lua 脚本占用超过 lua-time-limit
# 错误 3: 复制内存超限
# BUSY Redis is replica syncing with diskless loading disabled
# 原因:从节点复制时内存不足
# 错误 4: 子进程内存超限
# MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk
# 原因:fork 子进程时内存不足(Linux overcommit_memory)
2.2 错误日志分析
bash
# ===== 查看 Redis 错误日志 =====
# 默认日志位置
# /var/log/redis/redis-server.log
# 或通过 CONFIG GET logfile 查看
# 查看最近错误日志
TAIL -100 /var/log/redis/redis-server.log
# 常见 OOM 相关日志
# 1. Out of memory
# 2. Client asked for something bigger than maxmemory
# 3. Memory usage reached maxmemory set
# 4. Cannot allocate memory
# 实时监控错误日志
TAIL -f /var/log/redis/redis-server.log | grep -i "oom\|error\|memory"
2.3 客户端错误捕获
bash
# ===== 客户端连接 OOM =====
# Redis CLI 捕获 OOM 错误
redis-cli INFO stats | grep -E "total_commands_processed|rejected_commands"
# 查看被拒绝的命令
redis-cli INFO stats | grep -i "rejected"
# total_connections_received: 总连接数
# total_commands_processed: 总命令数
# rejected_commands: 被拒绝的命令数
# 实时监控命令拒绝率
redis-cli --latency-history
# 查看慢查询
redis-cli SLOWLOG GET 10
2.4 生产环境 OOM 案例
bash
# ===== 案例 1: 大 Key 导致 OOM =====
# 现象:突然 OOM,重启后正常但很快再次 OOM
# 排查:查看大 Key
redis-cli --bigkeys
# 输出示例:
# Scanning 50,000 keys in 0.73 seconds
# Big key (hash): 12,345 bytes, 10,000 fields
# Big key (string): 5,432,100 bytes
# Big key (list): 100,000 items, 2,000,000 bytes
# Big key (set): 50,000 members, 1,500,000 bytes
# ===== 案例 2: 内存泄漏 =====
# 现象:内存持续增长,不回落
# 排查:监控内存变化
redis-cli INFO memory | grep used_memory
# 每分钟记录一次,观察趋势
# 查看持久化状态
redis-cli INFO persistence
# aof_current_size: AOF 文件大小
# aof_base_size: AOF 重写基础大小
# ===== 案例 3: 淘汰策略不当 =====
# 现象:设置了过期时间但内存仍然增长
# 排查:检查过期策略
redis-cli CONFIG GET maxmemory-policy
# 如果是 noeviction,需要修改
# 检查过期 key 数量
redis-cli DBSIZE
redis-cli KEYS "*" | wc -l
三、大 Key 排查
3.1 查找大 Key
bash
# ===== 查找大 Key 工具 =====
# 方法 1: redis-cli --bigkeys
redis-cli --bigkeys
# 输出示例:
# Scanning 100,000 keys in database 0
# Big key (string): key:string:large (10000000 bytes)
# Big key (hash): key:hash:users (5000000 bytes, 100000 fields)
# Big key (list): key:list:messages (3000000 bytes, 10000 items)
# Big key (set): key:set:tags (2000000 bytes, 50000 members)
# Big key (zset): key:zset:rankings (4000000 bytes, 30000 members)
# Big key (stream): key:stream:events (8000000 bytes, 20000 entries)
# 方法 2: memory usage
redis-cli --scan | head -100 | xargs -I {} redis-cli MEMORY USAGE {}
# 方法 3: SCAN + MEMORY
redis-cli SCAN 0 MATCH "*" COUNT 1000 | while read key
do
size=$(redis-cli MEMORY USAGE "$key" 2>/dev/null)
if [ "$size" -gt 1048576 ]; then # > 1MB
echo "$key: $size bytes"
fi
done
# 方法 4: Python 脚本
```python
import redis
import sys
r = redis.Redis(host='localhost', port=6379, db=0)
def find_big_keys(threshold_bytes=10485760):
"""查找大于 threshold_bytes 的 key"""
big_keys = []
cursor = 0
while True:
cursor, keys = r.scan(cursor, count=1000)
for key in keys:
key_type = r.type(key).decode()
if key_type == 'string':
size = r.memory_usage(key) or 0
elif key_type in ('list', 'set', 'zset', 'hash', 'stream'):
size = r.memory_usage(key) or 0
else:
size = 0
if size > threshold_bytes:
big_keys.append({
'key': key.decode(),
'type': key_type,
'size': size,
'size_mb': round(size / 1048576, 2)
})
if cursor == 0:
break
# 按大小排序
big_keys.sort(key=lambda x: x['size'], reverse=True)
# 输出
print(f"Found {len(big_keys)} big keys (> {threshold_bytes / 1048576}MB)")
print("-" * 80)
for item in big_keys[:20]:
print(f"{item['key']}: {item['type']}, {item['size_mb']}MB")
return big_keys
if __name__ == '__main__':
threshold = int(sys.argv[1]) if len(sys.argv) > 1 else 10485760 # 默认 10MB
find_big_keys(threshold)
3.2 分析 Key 类型
bash
# ===== 分析 Key 类型分布 =====
# 方法 1: redis-cli --type
redis-cli --type string | wc -l # String 类型数量
redis-cli --type hash | wc -l # Hash 类型数量
redis-cli --type list | wc -l # List 类型数量
redis-cli --type set | wc -l # Set 类型数量
redis-cli --type zset | wc -l # Zset 类型数量
# 方法 2: 统计每种类型数量和内存
redis-cli INFO keyspace
# 输出示例:
# db0:keys=100000,expires=50000,avg_ttl=3600000
# 方法 3: 使用 TYPE 统计
redis-cli --scan | xargs -I {} redis-cli TYPE {} | sort | uniq -c
# 方法 4: Python 统计
```python
import redis
from collections import Counter
r = redis.Redis(host='localhost', port=6379, db=0)
def analyze_key_types():
"""统计 Key 类型分布"""
type_counts = Counter()
cursor = 0
while True:
cursor, keys = r.scan(cursor, count=1000)
for key in keys:
key_type = r.type(key).decode()
type_counts[key_type] += 1
if cursor == 0:
break
print("Key Type Distribution:")
print("-" * 40)
for key_type, count in type_counts.most_common():
print(f"{key_type}: {count}")
return type_counts
analyze_key_types()
3.3 大 Key 危险操作
bash
# ===== 大 Key 危险操作警告 =====
# ❌ 危险操作:这些命令会阻塞 Redis
# KEYS pattern # 全表扫描,阻塞所有请求
# FLUSHDB / FLUSHALL # 清空数据库,阻塞所有请求
# SMEMBERS key # 获取 Set 所有成员,可能阻塞
# LRANGE key 0 -1 # 获取 List 所有元素,可能阻塞
# HGETALL key # 获取 Hash 所有字段,可能阻塞
# DEL key # 删除大 Key,阻塞主线程
# ✅ 安全操作:异步删除
# UNLINK key # 异步删除,不阻塞
# UNLINK key1 key2 key3 # 批量异步删除
# ✅ 安全操作:渐进式删除
# SCAN + DEL
# ✅ 安全操作:渐进式遍历
# SCAN cursor [MATCH pattern] [COUNT count]
# SSCAN key cursor [MATCH pattern] [COUNT count]
# HSCAN key cursor [MATCH pattern] [COUNT count]
# ZSCAN key cursor [MATCH pattern] [COUNT count]
3.4 大 Key 安全删除
python
# ===== 大 Key 安全删除脚本 =====
import redis
import time
r = redis.Redis(host='localhost', port=6379, db=0)
def safe_delete_key(key, batch_size=1000):
"""安全删除大 Key"""
key_type = r.type(key).decode()
if key_type == 'none':
print(f"Key {key} does not exist")
return
print(f"Deleting key: {key}, type: {key_type}")
if key_type == 'string':
# String 类型直接删除
r.unlink(key)
print(f"Deleted {key}")
elif key_type == 'list':
# List 类型:从右弹出直到为空
while r.llen(key) > 0:
r.ltrim(key, 0, -batch_size-1)
r.unlink(key)
print(f"Deleted {key}")
elif key_type == 'set':
# Set 类型:批量删除成员
cursor = 0
while True:
cursor, members = r.sscan(key, cursor, count=batch_size)
if members:
r.srem(key, *members)
if cursor == 0:
break
r.unlink(key)
print(f"Deleted {key}")
elif key_type == 'hash':
# Hash 类型:批量删除字段
while r.hlen(key) > 0:
cursor = 0
fields_to_delete = []
while len(fields_to_delete) < batch_size:
cursor, fields = r.hscan(key, cursor, count=100)
fields_to_delete.extend(fields.keys())
if cursor == 0:
break
if fields_to_delete:
r.hdel(key, *fields_to_delete[:batch_size])
r.unlink(key)
print(f"Deleted {key}")
elif key_type == 'zset':
# Zset 类型:批量删除
while r.zcard(key) > 0:
r.zremrangebyrank(key, 0, batch_size-1)
r.unlink(key)
print(f"Deleted {key}")
elif key_type == 'stream':
# Stream 类型:删除所有条目
while r.xlen(key) > 0:
r.xtrim(key, maxlen=0, approximate=False)
r.unlink(key)
print(f"Deleted {key}")
def batch_delete_keys(pattern, batch_size=1000):
"""批量删除匹配 pattern 的 Key"""
cursor = 0
deleted = 0
while True:
cursor, keys = r.scan(cursor, match=pattern, count=batch_size)
if keys:
# 使用 UNLINK 异步删除
r.unlink(*keys)
deleted += len(keys)
print(f"Deleted {len(keys)} keys, total: {deleted}")
if cursor == 0:
break
print(f"Total deleted: {deleted} keys")
return deleted
四、内存分析工具
4.1 Redis 内置命令
bash
# ===== 内存分析命令 =====
# 1. MEMORY USAGE - 查看 Key 内存占用
redis-cli MEMORY USAGE user:12345
# 返回字节数
# 2. MEMORY STATS - 查看详细内存统计
redis-cli MEMORY STATS
# 输出示例:
# peak.allocated: 1048576000
# total.allocated: 524288000
# startup.allocated: 10485760
# replication.allocated: 0
# clients.allocated: 1048576
# aof.allocated: 0
# overhead.allocated: 5242880
# client.normal.size: 1048576
# client.slaves.size: 0
# aof.buffer.size: 0
# 3. MEMORY DOCTOR - 内存诊断
redis-cli MEMORY DOCTOR
# 输出建议:
# 1. Peak memory: 1GB
# 2. High memory fragmentation: consider using MEMORY PURGE
# 3. Consider using smaller keys or memory-efficient data types
# 4. OBJECT ENCODING - 查看编码方式
redis-cli OBJECT ENCODING user:12345
# int: 整数
# embstr: 短字符串
# raw: 长字符串
# hashtable: Hash
# skiplist: Zset
# quicklist: List
# 5. OBJECT FREQ - 查看 Key 访问频率
redis-cli OBJECT FREQ hot:key:12345
4.2 RDB 离线分析
bash
# ===== RDB 离线分析 =====
# 方法 1: redis-rdb-tools
# 安装: pip install rdbtools
# 导出内存分析报告
rdb --command memory /var/redis/dump.rdb -c json > memory_report.json
# 查看 Top N 大 Key
rdb --bigkeys /var/redis/dump.rdb
# 导出 CSV 格式
rdb -c memory /var/redis/dump.rdb > memory.csv
# 方法 2: 分析 CSV
cat memory.csv | head -20
# key,database,type,size_in_bytes,len
# user:1000,0,string,1048576,1048576
# session:abc123,0,hash,524288,100
# 方法 3: Python 分析脚本
```python
import rdbtools
from rdbtools.parser import RdbParser
import sys
def analyze_rdb(rdb_file):
"""分析 RDB 文件"""
parser = RdbParser()
stats = parser.parse(rdb_file)
# Top 20 大 Key
print("Top 20 Big Keys:")
print("-" * 80)
sorted_keys = sorted(stats.key_sizes.items(),
key=lambda x: x[1], reverse=True)[:20]
for key, size in sorted_keys:
size_mb = size / 1048576
print(f"{key}: {size_mb:.2f}MB")
# 类型分布
print("\nKey Type Distribution:")
print("-" * 40)
type_counts = {}
for key, key_type in stats.key_types.items():
type_counts[key_type] = type_counts.get(key_type, 0) + 1
for key_type, count in sorted(type_counts.items(),
key=lambda x: x[1], reverse=True):
print(f"{key_type}: {count}")
analyze_rdb(sys.argv[1])
4.3 在线监控工具
python
# ===== 在线内存监控脚本 =====
import redis
import time
import psutil
from datetime import datetime
r = redis.Redis(host='localhost', port=6379, db=0)
def monitor_memory(interval=5, duration=300):
"""内存监控"""
start_time = time.time()
prev_memory = None
print("Redis Memory Monitor")
print("=" * 80)
print(f"{'Time':<20} {'Used':<15} {'Peak':<15} {'RSS':<15} {'Frag':<10}")
print("-" * 80)
while time.time() - start_time < duration:
info = r.info('memory')
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
used = info['used_memory_human']
peak = info['used_memory_peak_human']
rss = info['used_memory_rss_human']
frag = f"{info['mem_fragmentation_ratio']:.2f}"
# 计算增长率
if prev_memory:
growth = info['used_memory'] - prev_memory
growth_rate = growth / (interval * 1024 * 1024)
print(f"{now} {used:<15} {peak:<15} {rss:<15} {frag:<10} +{growth_rate:.2f}MB/s")
else:
print(f"{now} {used:<15} {peak:<15} {rss:<15} {frag:<10}")
prev_memory = info['used_memory']
time.sleep(interval)
def detect_memory_leak():
"""检测内存泄漏"""
samples = []
print("Memory Leak Detection (10 samples)")
print("=" * 80)
for i in range(10):
info = r.info('memory')
used = info['used_memory']
samples.append(used)
if i > 0:
growth = used - samples[i-1]
growth_mb = growth / 1048576
trend = "↑" if growth > 0 else "↓"
print(f"Sample {i+1}: {used/1048576:.2f}MB, change: {trend}{abs(growth_mb):.2f}MB")
else:
print(f"Sample {i+1}: {used/1048576:.2f}MB")
time.sleep(2)
# 分析趋势
if samples[-1] > samples[0] * 1.1:
print("\n⚠️ Warning: Memory is growing!")
else:
print("\n✓ Memory is stable")
# 运行监控
if __name__ == '__main__':
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'leak':
detect_memory_leak()
else:
monitor_memory(interval=5, duration=60)
4.4 Prometheus 监控
yaml
# ===== Prometheus 监控配置 =====
# prometheus.yml
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['localhost:9121'] # redis_exporter
# redis_exporter 启动
# redis_exporter --redis.addr redis://localhost:6379
promql
# ===== Prometheus 查询 =====
# 内存使用率
redis_memory_used_bytes / redis_memory_max_bytes * 100
# 内存增长率
rate(redis_memory_used_bytes[5m])
# 内存碎片率
redis_mem_fragmentation_ratio
# Key 数量
redis_db_keys
# 命令拒绝数
rate(redis_commands_processed_total{rejected="true"}[5m])
# 告警规则
groups:
- name: redis-alerts
rules:
- alert: RedisOOM
expr: redis_memory_used_bytes > redis_memory_max_bytes * 0.9
for: 1m
labels:
severity: critical
annotations:
summary: "Redis 内存使用率超过 90%"
description: "Redis 内存使用率 {{ $value }}%,即将 OOM"
- alert: RedisHighFragmentation
expr: redis_mem_fragmentation_ratio > 1.5
for: 5m
labels:
severity: warning
annotations:
summary: "Redis 内存碎片率过高"
description: "内存碎片率 {{ $value }}"
五、内存恢复方案
5.1 紧急恢复
bash
# ===== 紧急恢复步骤 =====
# 1. 临时扩大内存(紧急)
redis-cli CONFIG SET maxmemory 2gb
# 2. 修改淘汰策略(紧急)
# 如果当前是 noeviction,改为 allkeys-lru
redis-cli CONFIG SET maxmemory-policy allkeys-lru
# 3. 手动清理内存
# 清理过期 key
redis-cli --scan --pattern "*:session:*" | xargs -I {} redis-cli EXPIRE {} 1
# 一分钟后过期
# 4. 执行内存碎片整理
redis-cli MEMORY PURGE
# 5. 清理大 Key
# 识别大 Key
redis-cli --bigkeys
# 删除不需要的大 Key
redis-cli UNLINK big:string:key
redis-cli UNLINK big:hash:users
# 6. 调整 AOF 持久化
# 如果使用 AOF rewrite,可能占用大量内存
redis-cli BGREWRITEAOF # 异步执行,不会阻塞
# 7. 重启 Redis(最后手段)
redis-cli SHUTDOWN NOSAVE # 不保存数据
# 启动 Redis
# redis-server /etc/redis/redis.conf
5.2 永久配置
bash
# ===== 永久配置修改 =====
# 编辑 redis.conf
vim /etc/redis/redis.conf
# 设置合理的 maxmemory
# maxmemory 2gb
maxmemory 2147483648 # 2GB
# 设置淘汰策略
# maxmemory-policy allkeys-lru
maxmemory-policy allkeys-lru
# 开启内存碎片自动清理(Redis 4.0+)
# activedefrag yes
activedefrag yes
# 设置碎片清理阈值
# active-defrag-ignore-bytes 100mb
active-defrag-ignore-bytes 100mb
# active-defrag-threshold-lower 10
active-defrag-threshold-lower 10
# active-defrag-threshold-upper 100
active-defrag-threshold-upper 100
# 重启生效
systemctl restart redis
5.3 内存优化策略
bash
# ===== 内存优化策略 =====
# 1. 使用高效编码
# 查看 Key 编码
redis-cli OBJECT ENCODING user:12345
# 设置小数字为 int 编码(自动)
SET user:123:score 100 # 自动使用 int 编码
# 2. 合理使用数据结构
# ❌ 不要存储大字符串
SET cache:page:123 "very long content..."
# ✅ 使用 Hash 代替多个 String
HSET cache:page:123 title "xxx" content "xxx" author "xxx"
# 3. 设置合理的过期时间
# 查看 TTL
redis-cli TTL user:12345
# 设置过期时间
EXPIRE user:session:123 3600 # 1小时
# 设置随机过期(避免 key 集中过期)
EXPIRE user:session:123 $((3600 + RANDOM % 600)) # 1小时 + 0-10分钟随机
# 4. 使用压缩(Redis 6.0+)
# 注意:压缩会消耗 CPU
SET key "large_content" KOMPRESS
# 5. 使用 Pipeline 减少连接数
redis-cli --pipe <<EOF
SET key1 value1
SET key2 value2
SET key3 value3
GET key1
EOF
5.4 客户端优化
python
# ===== 客户端内存优化 =====
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 1. 使用连接池
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=50)
r = redis.Redis(connection_pool=pool)
# 2. 使用 Pipeline 批量操作
pipe = r.pipeline()
for i in range(1000):
pipe.set(f'user:{i}', f'value:{i}')
pipe.execute()
# 3. 使用 Scan 代替 KEYS
cursor = 0
while True:
cursor, keys = r.scan(cursor, match='user:*', count=100)
# 处理 keys
if cursor == 0:
break
# 4. 使用 UNLINK 代替 DEL
r.unlink('big:key:1', 'big:key:2', 'big:key:3')
# 5. 合理设置客户端超时
r = redis.Redis(host='localhost', port=6379, socket_timeout=5,
socket_connect_timeout=5)
# 6. 使用 Lua 脚本减少网络往返
LUA_SCRIPT = """
local key = KEYS[1]
local value = redis.call('GET', key)
if value then
redis.call('DEL', key)
end
return value
"""
result = r.eval(LUA_SCRIPT, 1, 'key:name')
六、生产案例
6.1 案例:缓存雪崩
python
# ===== 案例:缓存雪崩导致 OOM =====
# 问题:大量 key 同时过期,瞬间压力打到数据库
# 现象:Redis 内存突然增长,CPU 飙升
# 错误代码
def get_user(user_id):
# 直接从 Redis 获取,没有就查数据库
user = redis.get(f'user:{user_id}')
if not user:
user = db.query('SELECT * FROM users WHERE id = ?', user_id)
redis.set(f'user:{user_id}', json.dumps(user))
return json.loads(user)
# 问题:
# 1. 没有设置过期时间或过期时间相同
# 2. 同时过期导致雪崩
# 3. 数据库压力导致响应慢,Redis 连接堆积
# ===== 解决方案 =====
# 1. 随机过期时间
def get_user(user_id):
user = redis.get(f'user:{user_id}')
if not user:
user = db.query('SELECT * FROM users WHERE id = ?', user_id)
user_json = json.dumps(user)
# 基础过期时间 + 随机偏移
expire = 3600 + random.randint(0, 600)
redis.setex(f'user:{user_id}', expire, user_json)
return json.loads(user)
# 2. 多级缓存
def get_user(user_id):
# 先从 L1 缓存获取(本地缓存)
local_cache = app_context.local_cache
if user_id in local_cache:
return local_cache[user_id]
# 再从 Redis 获取
user = redis.get(f'user:{user_id}')
if not user:
user = db.query('SELECT * FROM users WHERE id = ?', user_id)
user_json = json.dumps(user)
redis.setex(f'user:{user_id}', 3600, user_json)
local_cache[user_id] = json.loads(user_json)
return local_cache[user_id]
local_cache[user_id] = json.loads(user)
return local_cache[user_id]
# 3. 分布式锁保护
def get_user(user_id):
key = f'user:{user_id}'
# 尝试获取锁
lock_key = f'lock:{key}'
lock = redis.set(lock_key, '1', nx=True, ex=10)
if lock:
try:
user = db.query('SELECT * FROM users WHERE id = ?', user_id)
user_json = json.dumps(user)
redis.setex(key, 3600, user_json)
finally:
redis.delete(lock_key)
return user
# 没获取到锁,等待后重试
time.sleep(0.1)
return get_user(user_id)
6.2 案例:热 Key 打穿
python
# ===== 案例:热 Key 打穿 Redis =====
# 问题:某个 key 被大量请求
# 现象:单 Key 内存占用过高,CPU 单核打满
# 错误代码
def get_hot_product(product_id):
# 每次都查 Redis
return redis.get(f'hot:product:{product_id}')
# 问题:
# 1. 单个 key 访问量巨大
# 2. Redis CPU 单线程,可能打满
# 3. 内存被这个 key 占满
# ===== 解决方案 =====
# 1. Key 分片
def get_hot_product_sharded(product_id):
# 将单个 key 分成多个 shard
shard_id = hash(product_id) % 10
key = f'hot:product:{product_id}:shard:{shard_id}'
return redis.get(key)
# 2. 本地缓存 + Redis
from functools import lru_cache
@lru_cache(maxsize=10000)
def get_hot_product_local(product_id):
value = redis.get(f'hot:product:{product_id}')
return value
def get_hot_product(product_id):
# 先查本地缓存
value = get_hot_product_local(product_id)
if value:
return value
# 本地缓存没有,查 Redis
value = redis.get(f'hot:product:{product_id}')
if value:
get_hot_product_local.cache_clear()
get_hot_product_local(product_id)
return value
# 3. 热点 Key 监控
def monitor_hot_keys():
"""监控热点 Key"""
samples = {}
while True:
keys = redis.scan_iter(match='hot:*', count=100)
for key in keys:
# 记录访问频率
access_count = redis.object('FREQ', key) or 0
samples[key] = access_count
# 分析热点
hot_keys = sorted(samples.items(), key=lambda x: x[1], reverse=True)[:10]
print(f"Hot keys: {hot_keys}")
time.sleep(60)
6.3 案例:内存泄漏
python
# ===== 案例:内存泄漏 =====
# 问题:内存持续增长,不回落
# 排查:没有设置过期时间,或者有引用未释放
# 错误代码
class SessionManager:
def __init__(self):
self.redis = redis.Redis()
def create_session(self, user_id):
session_id = str(uuid.uuid4())
# 没有设置过期时间!
self.redis.set(f'session:{session_id}', json.dumps({
'user_id': user_id,
'created_at': time.time()
}))
return session_id
def delete_session(self, session_id):
self.redis.delete(f'session:{session_id}')
# 等等,delete_session 根本没被调用!
# 问题:
# 1. Session 没有设置过期时间
# 2. 删除逻辑有 bug,session 永远不会被清理
# 3. 内存只增不减
# ===== 解决方案 =====
# 1. 设置过期时间
def create_session(self, user_id):
session_id = str(uuid.uuid4())
self.redis.setex(f'session:{session_id}', 86400, json.dumps({ # 24小时
'user_id': user_id,
'created_at': time.time()
}))
return session_id
# 2. 使用 TTL 监控过期
def cleanup_expired_sessions(self):
"""清理过期 session"""
cursor = 0
deleted = 0
while True:
cursor, keys = self.redis.scan(cursor, match='session:*', count=100)
for key in keys:
ttl = self.redis.ttl(key)
if ttl == -1: # 没有过期时间
self.redis.expire(key, 86400) # 设置过期时间
if cursor == 0:
break
return deleted
# 3. 使用 Sorted Set 管理过期
def create_session_zset(self, user_id):
session_id = str(uuid.uuid4())
now = time.time()
# 使用 Sorted Set,score 为过期时间戳
self.redis.zadd('active_sessions', {session_id: now + 86400})
self.redis.hset(f'session:{session_id}', mapping={
'user_id': user_id,
'created_at': now
})
return session_id
def cleanup_expired_sessions_zset(self):
"""清理过期 session"""
now = time.time()
# 删除过期的 session
expired = self.redis.zrangebyscore('active_sessions', '-inf', now)
for session_id in expired:
self.redis.delete(f'session:{session_id}')
self.redis.zrem('active_sessions', session_id)
return len(expired)
七、总结
7.1 排查流程
#mermaid-svg-u2Gp95jSo2WYgVKo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-u2Gp95jSo2WYgVKo .error-icon{fill:#552222;}#mermaid-svg-u2Gp95jSo2WYgVKo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-u2Gp95jSo2WYgVKo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-u2Gp95jSo2WYgVKo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-u2Gp95jSo2WYgVKo .marker.cross{stroke:#333333;}#mermaid-svg-u2Gp95jSo2WYgVKo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-u2Gp95jSo2WYgVKo p{margin:0;}#mermaid-svg-u2Gp95jSo2WYgVKo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-u2Gp95jSo2WYgVKo .cluster-label text{fill:#333;}#mermaid-svg-u2Gp95jSo2WYgVKo .cluster-label span{color:#333;}#mermaid-svg-u2Gp95jSo2WYgVKo .cluster-label span p{background-color:transparent;}#mermaid-svg-u2Gp95jSo2WYgVKo .label text,#mermaid-svg-u2Gp95jSo2WYgVKo span{fill:#333;color:#333;}#mermaid-svg-u2Gp95jSo2WYgVKo .node rect,#mermaid-svg-u2Gp95jSo2WYgVKo .node circle,#mermaid-svg-u2Gp95jSo2WYgVKo .node ellipse,#mermaid-svg-u2Gp95jSo2WYgVKo .node polygon,#mermaid-svg-u2Gp95jSo2WYgVKo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-u2Gp95jSo2WYgVKo .rough-node .label text,#mermaid-svg-u2Gp95jSo2WYgVKo .node .label text,#mermaid-svg-u2Gp95jSo2WYgVKo .image-shape .label,#mermaid-svg-u2Gp95jSo2WYgVKo .icon-shape .label{text-anchor:middle;}#mermaid-svg-u2Gp95jSo2WYgVKo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-u2Gp95jSo2WYgVKo .rough-node .label,#mermaid-svg-u2Gp95jSo2WYgVKo .node .label,#mermaid-svg-u2Gp95jSo2WYgVKo .image-shape .label,#mermaid-svg-u2Gp95jSo2WYgVKo .icon-shape .label{text-align:center;}#mermaid-svg-u2Gp95jSo2WYgVKo .node.clickable{cursor:pointer;}#mermaid-svg-u2Gp95jSo2WYgVKo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-u2Gp95jSo2WYgVKo .arrowheadPath{fill:#333333;}#mermaid-svg-u2Gp95jSo2WYgVKo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-u2Gp95jSo2WYgVKo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-u2Gp95jSo2WYgVKo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u2Gp95jSo2WYgVKo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-u2Gp95jSo2WYgVKo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u2Gp95jSo2WYgVKo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-u2Gp95jSo2WYgVKo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-u2Gp95jSo2WYgVKo .cluster text{fill:#333;}#mermaid-svg-u2Gp95jSo2WYgVKo .cluster span{color:#333;}#mermaid-svg-u2Gp95jSo2WYgVKo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-u2Gp95jSo2WYgVKo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-u2Gp95jSo2WYgVKo rect.text{fill:none;stroke-width:0;}#mermaid-svg-u2Gp95jSo2WYgVKo .icon-shape,#mermaid-svg-u2Gp95jSo2WYgVKo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-u2Gp95jSo2WYgVKo .icon-shape p,#mermaid-svg-u2Gp95jSo2WYgVKo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-u2Gp95jSo2WYgVKo .icon-shape .label rect,#mermaid-svg-u2Gp95jSo2WYgVKo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-u2Gp95jSo2WYgVKo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-u2Gp95jSo2WYgVKo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-u2Gp95jSo2WYgVKo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
OOM 发生
查看错误日志
检查内存使用
是否超过 maxmemory
检查淘汰策略
检查内存碎片
修改策略或扩大内存
执行 MEMORY PURGE
清理大 Key
恢复正常
7.2 排查命令速查表
| 命令 | 用途 |
|---|---|
INFO memory |
查看内存使用概况 |
MEMORY STATS |
详细内存统计 |
MEMORY USAGE key |
查看 Key 内存占用 |
MEMORY DOCTOR |
内存诊断 |
MEMORY PURGE |
清理内存碎片 |
OBJECT ENCODING key |
查看 Key 编码 |
--bigkeys |
查找大 Key |
SCAN |
安全遍历 Key |
UNLINK |
异步删除 Key |
7.3 解决方案汇总
| 问题 | 解决方案 |
|---|---|
| 内存溢出 | 扩大 maxmemory / 修改淘汰策略 |
| 大 Key | 分片 / 删除 / 优化结构 |
| 内存碎片 | MEMORY PURGE / activedefrag |
| Key 堆积 | 设置过期时间 / 清理过期 Key |
| 缓存雪崩 | 随机过期 / 多级缓存 / 分布式锁 |
| 热 Key | 分片 / 本地缓存 / 热点监控 |
| 内存泄漏 | 设置 TTL / 定期清理 |
7.4 最佳实践
| 实践 | 说明 |
|---|---|
| 合理设置 maxmemory | 保留 20% 冗余 |
| 选择合适淘汰策略 | 根据业务选择 |
| 设置过期时间 | 防止 Key 永不过期 |
| 随机过期时间 | 避免雪崩 |
| 使用 UNLINK | 异步删除大 Key |
| 开启碎片自动清理 | activedefrag yes |
| 监控内存指标 | Prometheus + Grafana |
| 定期分析 Key | 使用 rdb-tools |
本文基于 Redis 7.0 编写。如有问题欢迎评论区讨论!