Redis 内存管理与淘汰策略
2026/3/20大约 16 分钟
Redis 内存管理与淘汰策略
一、Redis 内存模型
1.1 Redis 为什么这么快
Redis 的高性能主要来源于以下几个方面:
1.2 Redis 内存组成
# 查看内存使用情况
redis-cli INFO memory
# 关键指标说明
used_memory:1073741824 # Redis 分配器分配的内存总量(字节)
used_memory_human:1.00G # 人类可读格式
used_memory_rss:1181116416 # 操作系统看到的内存(RSS)
used_memory_rss_human:1.10G
used_memory_peak:1073741824 # 内存使用峰值
used_memory_peak_human:1.00G
used_memory_overhead:52428800 # 管理开销(元数据)
used_memory_startup:809872 # 启动时消耗的内存
used_memory_dataset:1021312024 # 数据集大小
mem_fragmentation_ratio:1.10 # 内存碎片率
内存组成详解:
1.3 内存碎片
内存碎片是指分配器分配的内存与实际使用内存的差异。
# 内存碎片率
mem_fragmentation_ratio:1.10
# 判断标准:
# > 1.5:碎片率较高,需要关注
# > 2.0:碎片严重,需要处理
# < 1.0:内存不足,可能使用了 Swap
碎片产生原因:
- 频繁的修改操作:删除大量 key 后,内存无法完全回收
- 数据大小变化:value 大小频繁变化,导致重新分配
- 内存分配器特性:jemalloc 按固定规格分配内存
处理碎片的方法:
# 方法1:重启 Redis(简单但有影响)
redis-cli SHUTDOWN
redis-server /etc/redis/redis.conf
# 方法2:内存碎片整理(Redis 4.0+,推荐)
# 配置
activedefrag yes # 开启自动碎片整理
active-defrag-ignore-bytes 100mb # 碎片达到 100MB 才整理
active-defrag-threshold-lower 10 # 碎片率达到 10% 开始整理
active-defrag-threshold-upper 100 # 碎片率达到 100% 尽最大努力整理
active-defrag-cycle-min 5 # 整理占用 CPU 时间最小百分比
active-defrag-cycle-max 75 # 整理占用 CPU 时间最大百分比
# 手动触发碎片整理
redis-cli MEMORY DOCTOR
redis-cli MEMORY PURGE # 清理内存碎片(仅 jemalloc)
二、过期键删除策略
2.1 设置过期时间
# 设置过期时间
SET key value EX 3600 # 设置时指定过期时间(秒)
SET key value PX 3600000 # 毫秒
EXPIRE key 3600 # 对已存在的 key 设置过期时间
PEXPIRE key 3600000 # 毫秒
EXPIREAT key 1704067200 # Unix 时间戳(秒)
PEXPIREAT key 1704067200000 # 毫秒
# 查看过期时间
TTL key # 秒
PTTL key # 毫秒
# 移除过期时间
PERSIST key
2.2 过期键的存储
Redis 使用两个字典来管理键空间和过期时间:
2.3 过期键删除策略
Redis 使用两种策略配合删除过期键:
策略 1:惰性删除(Lazy Deletion)
优点:对 CPU 友好,只在访问时检查
缺点:可能有大量过期键占用内存
策略 2:定期删除(Periodic Deletion)
定期删除流程(每 100ms 执行)
for (i = 0; i < 16; i++) {
1. 从 expires 字典随机取 20 个 key
2. 删除其中已过期的 key
3. 如果过期 key 比例 > 25%,重复步骤 1
}
时间限制:每次最多执行 25ms,避免阻塞主线程
优点:可以定期清理过期键,减少内存占用
缺点:可能影响性能(需要平衡删除频率和时间)
2.4 过期键与持久化
RDB 处理
RDB 生成时:
- 遍历所有 key
- 检查是否过期,过期的不保存到 RDB
RDB 加载时:
- 主节点:跳过过期的 key
- 从节点:全部加载(由主节点同步删除命令)
AOF 处理
AOF 写入时:
- 不检查过期,正常写入命令
- 当 key 被惰性删除时,追加一条 DEL 命令
AOF 重写时:
- 遍历所有 key
- 跳过已过期的 key
三、内存淘汰策略
3.1 为什么需要内存淘汰
当 Redis 内存使用达到 maxmemory 限制时,需要淘汰部分数据来腾出空间:
# 设置最大内存限制
maxmemory 4gb
# 如果不设置(默认):
# 32位系统:最大 3GB
# 64位系统:无限制(使用全部可用内存)
3.2 八种淘汰策略
Redis 提供了 8 种内存淘汰策略(6.0 版本):
# 设置淘汰策略
maxmemory-policy allkeys-lru
| 策略 | 说明 | 适用场景 |
|---|---|---|
| noeviction | 不淘汰,内存满时返回错误 | 确保数据不丢失 |
| volatile-lru | 从设置过期时间的 key 中 LRU 淘汰 | 缓存 + 持久化混合 |
| allkeys-lru | 从所有 key 中 LRU 淘汰 | 纯缓存场景(推荐) |
| volatile-lfu | 从设置过期时间的 key 中 LFU 淘汰 | 热点数据保护 |
| allkeys-lfu | 从所有 key 中 LFU 淘汰 | 热点数据场景 |
| volatile-random | 从设置过期时间的 key 中随机淘汰 | 不关心访问频率 |
| allkeys-random | 从所有 key 中随机淘汰 | 均匀访问场景 |
| volatile-ttl | 淘汰最快过期的 key | 时效性优先 |
3.3 LRU 算法详解
LRU(Least Recently Used):淘汰最近最少使用的 key。
标准 LRU 实现
标准 LRU 的问题:
- 需要维护双向链表,额外内存开销
- 每次访问都需要移动节点,性能开销
Redis 近似 LRU
Redis 使用近似 LRU 算法,在每个 key 的 redisObject 中记录最后访问时间:
typedef struct redisObject {
unsigned type:4; // 数据类型
unsigned encoding:4; // 编码方式
unsigned lru:24; // LRU 时间戳(秒级,24位)
int refcount; // 引用计数
void *ptr; // 数据指针
} robj;
淘汰过程:
Redis 3.0 优化:引入淘汰池(eviction pool)
3.4 LFU 算法详解
LFU(Least Frequently Used):淘汰访问频率最低的 key。
Redis 4.0 引入 LFU 策略,复用 lru 字段:
// LRU 模式:24位全部用于时间戳
// LFU 模式:16位时间 + 8位计数器
计数器的衰减
为了让计数器反映"最近"的访问频率,Redis 使用概率衰减:
计数器衰减公式
counter = counter - (当前时间 - 上次访问时间) / lfu_decay_time
lfu-decay-time 1 # 每分钟衰减
示例:
- counter = 100
- 距上次访问 5 分钟
- 衰减后:counter = 100 - 5 = 95
计数器的增长
为了避免计数器过快到达上限(255),使用概率增长:
计数器增长公式
p = 1.0 / (counter * lfu_log_factor + 1)
if random() < p:
counter++
lfu-log-factor 10 # 对数因子
示例(factor=10):
- counter=0 时,100% 概率增加
- counter=10 时,约 9% 概率增加
- counter=100 时,约 1% 概率增加
- counter=255 时,约 0.4% 概率增加
# LFU 配置
lfu-log-factor 10 # 计数器增长因子,越大增长越慢
lfu-decay-time 1 # 计数器衰减时间(分钟)
3.5 策略选择建议
| 场景 | 推荐策略 | 说明 |
|---|---|---|
| 纯缓存 | allkeys-lru | 通用选择 |
| 热点数据明显 | allkeys-lfu | 保护热点数据 |
| 缓存 + 持久化数据 | volatile-lru/lfu | 只淘汰缓存数据 |
| 数据有时效性 | volatile-ttl | 优先淘汰快过期的 |
| 不允许淘汰 | noeviction | 内存满时返回错误 |
四、内存优化实践
4.1 Key 设计优化
# 避免过长的 key 名
# 不好
SET very_very_very_long_key_name_user_info_id_1001 "{...}"
# 好
SET u:1001 "{...}"
# 使用冒号分隔,保持可读性
SET user:1001:info "{...}"
SET user:1001:session "xxx"
# 批量操作时使用 Hash 代替多个 String
# 不好(每个 key 都有额外开销)
SET user:1001:name "张三"
SET user:1001:age "25"
SET user:1001:email "xxx@example.com"
# 好(一个 key 存储多个字段)
HSET user:1001 name "张三" age "25" email "xxx@example.com"
4.2 Value 设计优化
# 使用合适的数据类型
# 计数器用 String
SET counter:views 0
INCR counter:views
# 多字段对象用 Hash
HSET product:1001 name "iPhone" price 9999 stock 100
# 有序数据用 ZSet
ZADD ranking 100 user1 200 user2
# 使用压缩编码(小数据量时自动使用 ziplist)
# 控制 Hash、List、Set、ZSet 的元素数量和大小
# 避免大 key
# 单个 String value 不超过 10KB
# Hash、List 等元素数量不超过 5000
# 大数据考虑拆分
4.3 大 Key 处理
# 发现大 key
redis-cli --bigkeys
# 更详细的扫描
redis-cli --memkeys
# 或使用 SCAN + MEMORY USAGE
SCAN 0 COUNT 100
MEMORY USAGE key
# 删除大 key(避免阻塞)
# 不好:直接删除大 key 会阻塞
DEL bigkey
# 好:使用 UNLINK 异步删除(Redis 4.0+)
UNLINK bigkey
# 或者分批删除
# Hash:HSCAN + HDEL
# List:LTRIM 逐步缩小
# Set:SSCAN + SREM
# ZSet:ZSCAN + ZREM
4.4 数据压缩
# 在应用层压缩数据
# 示例:使用 LZ4 或 Snappy 压缩 JSON
import lz4.frame
# 存储时压缩
data = json.dumps(user_data)
compressed = lz4.frame.compress(data.encode())
redis.set('user:1001', compressed)
# 读取时解压
compressed = redis.get('user:1001')
data = lz4.frame.decompress(compressed).decode()
user_data = json.loads(data)
4.5 过期时间优化
# 为缓存数据设置过期时间
SET cache:user:1001 "{...}" EX 3600
# 避免大量 key 同时过期
# 添加随机偏移
import random
base_expire = 3600
random_offset = random.randint(0, 600) # 0-10分钟随机偏移
redis.setex(key, base_expire + random_offset, value)
# 惰性设置过期时间
# 访问时续期
GET key
EXPIRE key 3600
4.6 客户端缓存
Redis 6.0+ 支持客户端缓存,减少网络往返:
# 开启客户端缓存(Tracking)
CLIENT TRACKING ON
# 使用示例(需要客户端支持)
# Lettuce、Jedis 4.0+ 支持
五、内存监控与告警
5.1 关键监控指标
# 监控脚本
#!/bin/bash
# 获取内存信息
info=$(redis-cli INFO memory)
# 解析关键指标
used_memory=$(echo "$info" | grep "used_memory:" | cut -d: -f2 | tr -d '\r')
used_memory_rss=$(echo "$info" | grep "used_memory_rss:" | cut -d: -f2 | tr -d '\r')
maxmemory=$(redis-cli CONFIG GET maxmemory | tail -1)
mem_fragmentation_ratio=$(echo "$info" | grep "mem_fragmentation_ratio:" | cut -d: -f2 | tr -d '\r')
# 计算内存使用率
if [ "$maxmemory" -gt 0 ]; then
usage_percent=$(echo "scale=2; $used_memory * 100 / $maxmemory" | bc)
else
usage_percent=0
fi
# 告警阈值检查
# 内存使用率超过 80%
if (( $(echo "$usage_percent > 80" | bc -l) )); then
echo "WARN: Memory usage is ${usage_percent}%"
fi
# 碎片率超过 1.5
if (( $(echo "$mem_fragmentation_ratio > 1.5" | bc -l) )); then
echo "WARN: Memory fragmentation ratio is ${mem_fragmentation_ratio}"
fi
5.2 Prometheus + Grafana 监控
# prometheus.yml
scrape_configs:
- job_name: "redis"
static_configs:
- targets: ["redis-exporter:9121"]
# 常用指标
# redis_memory_used_bytes:已使用内存
# redis_memory_max_bytes:最大内存
# redis_mem_fragmentation_ratio:碎片率
# redis_expired_keys_total:过期 key 数量
# redis_evicted_keys_total:淘汰 key 数量
5.3 告警规则
# alertmanager rules
groups:
- name: redis
rules:
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Redis memory usage is high"
description: "Redis memory usage is {{ $value | humanizePercentage }}"
- alert: RedisFragmentationHigh
expr: redis_mem_fragmentation_ratio > 1.5
for: 10m
labels:
severity: warning
annotations:
summary: "Redis memory fragmentation is high"
- alert: RedisEvictedKeys
expr: increase(redis_evicted_keys_total[5m]) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Redis is evicting keys"
六、常见问题排查
6.1 内存使用异常增长
# 1. 检查是否有大 key
redis-cli --bigkeys
# 2. 分析内存使用
redis-cli MEMORY DOCTOR
# 3. 按类型统计
redis-cli INFO keyspace
# db0:keys=1000000,expires=500000,avg_ttl=3600000
# 4. 采样分析
redis-cli DEBUG OBJECT key
6.2 频繁触发淘汰
# 查看淘汰统计
redis-cli INFO stats | grep evicted
# evicted_keys:1000
# 可能原因:
# 1. maxmemory 设置过小
# 2. 数据量增长过快
# 3. 过期时间设置不合理
# 解决方案:
# 1. 增加 maxmemory
# 2. 优化数据结构,减少内存占用
# 3. 为更多 key 设置过期时间
# 4. 调整淘汰策略
6.3 内存碎片率过高
# 检查碎片率
redis-cli INFO memory | grep mem_fragmentation
# 解决方案
# 1. 开启自动碎片整理(Redis 4.0+)
CONFIG SET activedefrag yes
# 2. 手动整理
MEMORY PURGE
# 3. 如果碎片率持续过高,考虑重启
# 4. 使用 jemalloc(默认)替代 libc
七、总结
内存管理要点
- 理解内存组成:数据 + 管理开销 + 碎片
- 设置 maxmemory:生产环境必须设置
- 选择合适的淘汰策略:根据场景选择 LRU/LFU
优化建议
- Key 设计:简短、规范、避免大 key
- Value 设计:使用合适的数据类型、控制大小
- 过期时间:合理设置、避免集中过期
- 监控告警:关注内存使用率、碎片率、淘汰数量
淘汰策略速查
| 场景 | 策略 |
|---|---|
| 通用缓存 | allkeys-lru |
| 热点数据 | allkeys-lfu |
| 部分持久化 | volatile-lru/lfu |
| 时效优先 | volatile-ttl |
| 禁止淘汰 | noeviction |