一、核心概念
Redis内存淘汰策略(Memory Eviction Policy)是指当Redis使用的内存达到 maxmemory 限制时,Redis会根据配置的淘汰策略选择并删除部分数据,以腾出空间存储新数据。
与过期策略的区别:
- 过期策略: 处理已过期的key(基于TTL时间)
- 内存淘汰策略: 处理内存不足的情况(不限于过期key)
二、8种内存淘汰策略
Redis提供了8种内存淘汰策略,可通过 maxmemory-policy 配置:
1. noeviction(默认策略)
- 行为: 不淘汰任何数据,内存满时直接返回错误
- 适用场景: 数据绝对不能丢失的场景,如数据库缓存
- 优点: 保证数据完整性
- 缺点: 内存满后无法写入新数据
# 当内存不足时返回错误
127.0.0.1:6379> SET key1 value1
(error) OOM command not allowed when used memory > 'maxmemory'
2. allkeys-lru(最常用)
- 行为: 从所有key中,使用LRU算法淘汰最近最少使用的key
- 适用场景: 通用缓存场景,访问热点数据
- 优点: 符合大多数业务场景,保留热点数据
- 推荐指数: ⭐⭐⭐⭐⭐
3. allkeys-lfu(Redis 4.0+)
- 行为: 从所有key中,使用LFU算法淘汰访问频率最低的key
- 适用场景: 区分热点数据,某些key访问频率持续较高
- 优点: 比LRU更精准,考虑访问频率而非时间
- 推荐指数: ⭐⭐⭐⭐
4. allkeys-random
- 行为: 从所有key中随机淘汰
- 适用场景: 所有key访问概率相似,或测试环境
- 优点: 实现简单,速度快
- 缺点: 可能淘汰热点数据
5. volatile-lru
- 行为: 仅从设置了过期时间的key中,使用LRU算法淘汰
- 适用场景: 区分持久数据和临时数据,只淘汰临时缓存
- 注意: 如果没有设置过期时间的key,则无法淘汰,行为类似noeviction
6. volatile-lfu(Redis 4.0+)
- 行为: 仅从设置了过期时间的key中,使用LFU算法淘汰
- 适用场景: 同上,但更关注访问频率
7. volatile-random
- 行为: 仅从设置了过期时间的key中随机淘汰
- 适用场景: 设置了过期时间的都是可淘汰数据,且无明显访问偏好
8. volatile-ttl
- 行为: 仅从设置了过期时间的key中,淘汰TTL最小(即将过期)的key
- 适用场景: 优先清理快要过期的数据
- 优点: 符合”即将过期的数据价值更低”的逻辑
三、LRU与LFU算法原理
1. LRU(Least Recently Used)算法
标准LRU原理:
- 维护一个双向链表,按访问时间排序
- 访问一个key时,将其移到链表头部
- 淘汰时,删除链表尾部的key
Redis近似LRU实现(源码关键点):
Redis并未使用严格的LRU算法(需要额外的链表结构和大量内存),而是使用了近似LRU算法:
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:24; // 存储最后访问时间戳(秒级)或LFU数据
int refcount;
void *ptr;
} robj;
淘汰过程:
- 随机抽样N个key(默认5个,由
maxmemory-samples配置) - 从这N个key中选出最久未使用的key
- 删除该key
- 如果内存仍不足,重复上述步骤
int evict = 0;
for (int i = 0; i < server.maxmemory_samples; i++) {
robj *o = dictGetRandomKey(dict);
// 计算idle时间
unsigned long idle = estimateObjectIdleTime(o);
// 选择idle时间最长的
if (idle > max_idle) {
max_idle = idle;
evict = o;
}
}
// 淘汰选中的key
dbDelete(db, evict);
优点:
- 节省内存,无需维护链表
- 性能好,O(1)时间复杂度
缺点:
- 近似算法,不保证绝对准确
- 抽样数量(maxmemory-samples)越大越精确,但性能开销也越大
2. LFU(Least Frequently Used)算法
原理:
- 记录key的访问频率(访问次数)
- 淘汰访问频率最低的key
Redis LFU实现:
Redis复用了 redisObject 的24位lru字段存储LFU信息:
- 高16位:存储上次递减时间(分钟级)
- 低8位:存储访问频率计数器(0-255)
// LFU字段结构
// [16位时间戳][8位计数器]
// 访问时增加计数器
void updateLFU(robj *o) {
unsigned long counter = LFUDecrAndReturn(o); // 先衰减
counter = LFULogIncr(counter); // 再增加
o->lru = (LFUGetTimeInMinutes() << 8) | counter;
}
关键特性:
- 对数递增: 访问次数不是线性增加,而是按对数递增,避免老数据计数过高
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand() / RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
double p = 1.0 / (baseval * server.lfu_log_factor + 1);
if (r < p) counter++;
return counter;
}
- 时间衰减: 长时间未访问的key,其计数器会逐渐衰减
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = o->lru >> 8; // 获取上次衰减时间
unsigned long counter = o->lru & 255; // 获取计数器
// 计算需要衰减的次数
unsigned long num_periods = server.lfu_decay_time ?
LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
// 衰减计数器
if (num_periods)
counter = (num_periods > counter) ? 0 : counter - num_periods;
return counter;
}
配置参数:
# LFU计数器对数增长因子(默认10)
lfu-log-factor 10
# LFU计数器衰减时间(分钟,默认1)
lfu-decay-time 1
四、策略选择建议
场景一:通用Web应用缓存
maxmemory 2gb
maxmemory-policy allkeys-lru
maxmemory-samples 5 # 默认值,可根据需要调整
理由: 大多数Web应用符合80/20原则,20%的热点数据占据80%的访问,LRU能很好地保留热点数据。
场景二:明确的热点数据访问模式
maxmemory 2gb
maxmemory-policy allkeys-lfu
lfu-log-factor 10
lfu-decay-time 1
理由: 某些key持续高频访问(如热门商品),LFU比LRU更精准。
场景三:混合持久数据和缓存数据
maxmemory 2gb
maxmemory-policy volatile-lru
理由:
- 持久数据不设置过期时间,永远不会被淘汰
- 缓存数据设置过期时间,内存不足时会被淘汰
// 持久数据(如配置信息)
redisTemplate.opsForValue().set("config:app", configData);
// 缓存数据(设置过期时间)
redisTemplate.opsForValue().set("cache:user:1001", userData, 3600, TimeUnit.SECONDS);
场景四:数据绝对不能丢失
maxmemory 2gb
maxmemory-policy noeviction
理由: 宁愿拒绝写入,也不能丢失数据。配合监控告警,及时扩容。
五、性能与监控
1. 查看当前策略
127.0.0.1:6379> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"
2. 动态修改策略
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lfu
OK
3. 监控淘汰情况
127.0.0.1:6379> INFO stats
# Stats
evicted_keys:5000 # 淘汰的key数量
expired_keys:10000 # 过期删除的key数量
keyspace_hits:1000000 # 命中次数
keyspace_misses:50000 # 未命中次数
关键指标:
evicted_keys:淘汰数量激增说明内存不足- 命中率 =
keyspace_hits / (keyspace_hits + keyspace_misses),命中率下降可能需要调整淘汰策略
六、答题总结
Redis提供8种内存淘汰策略,核心分类:
| 策略 | 范围 | 算法 | 使用场景 |
|---|---|---|---|
| allkeys-lru | 全部key | LRU近似 | 通用缓存(推荐) |
| allkeys-lfu | 全部key | LFU | 明确热点数据 |
| allkeys-random | 全部key | 随机 | 访问无偏好 |
| volatile-lru | 带过期时间 | LRU近似 | 混合持久和缓存数据 |
| volatile-lfu | 带过期时间 | LFU | 同上,关注频率 |
| volatile-ttl | 带过期时间 | 最小TTL | 优先清理即将过期 |
| volatile-random | 带过期时间 | 随机 | 临时数据随机淘汰 |
| noeviction | 不淘汰 | - | 数据不能丢失 |
LRU vs LFU:
- LRU: 关注访问时间,最近访问的保留,适合大部分场景
- LFU: 关注访问频率,高频访问的保留,更精准但计算复杂
Redis实现特点:
- 采用近似LRU/LFU算法,通过随机采样实现,节省内存
- 不需要额外的链表结构,利用redisObject的24位lru字段存储信息
- 可通过
maxmemory-samples调整精度(默认5)
生产环境建议:
- 通用场景选择
allkeys-lru - 设置合理的
maxmemory(通常为物理内存的70-80%) - 监控
evicted_keys和命中率,及时调整策略或扩容