Redis缓存淘汰与过期删除策略深度解析:内存不够怎么办?

难度:⭐⭐⭐⭐ | 适合人群:想优化Redis内存使用的开发者


💥 开场:一次"诡异"的内存爆满

时间: 周四下午
地点: 办公室
事件: 系统告警

告警消息: "Redis内存使用率95%!"

我: "怎么回事?昨天才70%啊?" 😰

哈吉米走过来: "你看看有多少key?"

我连上Redis:

redis> DBSIZE
(integer) 8542369  # 854万个key

redis> INFO memory
used_memory_human:15.2G
maxmemory:16G  # 最大内存16GB

我: "key这么多?我明明都设置了过期时间啊..." 🤔


随便查几个key:

redis> TTL user:12345
(integer) -1  # -1表示永不过期!

redis> TTL session:abc123
(integer) -1  # 又是-1!

redis> TTL product:99999
(integer) -1  # 还是-1!

我: "卧槽!过期时间怎么都没了???" 😱

南北绿豆: "你是不是用SET命令覆盖了?"

我翻代码:

// 初始化缓存(有过期时间)
redisTemplate.opsForValue().set("user:123", json, 1, TimeUnit.HOURS);

// 后来更新用户信息(忘了设置过期时间!)
redisTemplate.opsForValue().set("user:123", newJson);  // ← 这里!

南北绿豆: "SET命令会覆盖过期时间,你这样写过期时间就没了!"

我: "那现在怎么办?内存快满了..." 😓

阿西噶阿西: "内存满了Redis会自动淘汰key,但要配置好策略。来,我给你讲讲Redis的内存管理..."


🎯 第一问:过期Key的删除策略

三种删除策略

哈吉米: "Redis删除过期key有三种策略。"

策略1:定时删除(主动)

原理:

给每个key设置一个定时器
    ↓
到期时间一到,立即删除

优点:

  • ✅ 及时删除,内存友好
  • ✅ 过期立即删除

缺点:

  • ❌ 每个key一个定时器,CPU消耗大
  • ❌ key很多时,定时器太多

结论: Redis不用这种方式


策略2:惰性删除(被动)

原理:

不主动删除过期key
    ↓
访问key时才检查是否过期
    ↓
过期了才删除

流程:

sequenceDiagram
    participant Client
    participant Redis
    
    Note over Redis: key "user:1" 已过期<br/>但还在内存中
    
    Client->>Redis: GET user:1
    Redis->>Redis: 检查TTL
    Note over Redis: 发现已过期
    Redis->>Redis: 删除key
    Redis-->>Client: nil

优点:

  • ✅ CPU友好(只在访问时检查)
  • ✅ 实现简单

缺点:

  • ❌ 内存不友好(过期key可能长期占用内存)
  • ❌ 如果key永远不被访问,永远不会删除

结论: Redis使用,但不够


策略3:定期删除(主动)

原理:

每隔一段时间
    ↓
随机抽取一批key检查
    ↓
删除过期的key

Redis的实现:

默认每秒执行10次(每100ms一次):

1. 随机抽取20个有过期时间的key
2. 删除其中过期的key
3. 如果过期key超过25%(5个),重复步骤1
4. 单次执行时间不超过25ms

优点:

  • ✅ 平衡CPU和内存
  • ✅ 限制了执行时间,不影响性能

缺点:

  • ❌ 随机抽取,可能有漏网之鱼

结论: Redis使用


Redis使用的策略

南北绿豆: "Redis结合了惰性删除 + 定期删除!"

惰性删除:
    ↓
访问key时删除
    ↓
保证访问时拿到的一定是有效数据

定期删除:
    ↓
后台定期清理
    ↓
避免大量过期key占用内存

但还是可能有问题!


问题:内存依然会满

场景:
1. 写入速度 > 删除速度
2. 大量key过期但未被访问(定期删除抽取不到)
3. 没设置过期时间的key越来越多
    ↓
内存占用持续增长
    ↓
最终内存满了!
    ↓
怎么办?

阿西噶阿西: "这时候就需要内存淘汰策略!"


🔥 第二问:内存淘汰策略

什么是内存淘汰?

当内存达到maxmemory时,主动删除一些key腾出空间

配置:

# 最大内存
maxmemory 16gb

# 淘汰策略
maxmemory-policy allkeys-lru

8种淘汰策略

哈吉米: "Redis 4.0+有8种淘汰策略!"

1. noeviction(不淘汰,默认)

内存满了:
    ↓
拒绝所有写操作
    ↓
返回错误:OOM command not allowed when used memory > 'maxmemory'

适用: 数据绝对不能丢失的场景


2. allkeys-lru(推荐)

从所有key中
    ↓
使用LRU算法(Least Recently Used)
    ↓
淘汰最近最少使用的key

LRU算法:

key访问时间:
user:110分钟前访问
user:25分钟前访问
user:31分钟前访问

淘汰顺序:
user:1(最久未访问)→ user:2user:3

适用: 通用场景(推荐)


3. allkeys-random

从所有key中
    ↓
随机淘汰

适用: 访问频率相近的场景


4. allkeys-lfu(Redis 4.0+)

从所有key中
    ↓
使用LFU算法(Least Frequently Used)
    ↓
淘汰访问频率最低的key

LFU算法:

key访问次数:
user:1  → 访问1000user:2  → 访问100user:3  → 访问10次

淘汰顺序:
user:3(访问最少)→ user:2user:1

适用: 热点数据明显的场景


5. volatile-lru

从设置了过期时间的key中
    ↓
使用LRU算法淘汰

适用: 部分数据需要持久化,部分可淘汰


6. volatile-random

从设置了过期时间的key中
    ↓
随机淘汰

7. volatile-lfu(Redis 4.0+)

从设置了过期时间的key中
    ↓
使用LFU算法淘汰

8. volatile-ttl

从设置了过期时间的key中
    ↓
优先淘汰TTL最小的(最快过期的)

适用: 有明确优先级的场景


8种策略对比表

策略范围算法适用场景推荐度
noeviction-不淘汰不允许丢数据⭐⭐
allkeys-lru所有keyLRU通用场景⭐⭐⭐⭐⭐
allkeys-random所有key随机访问频率相近⭐⭐
allkeys-lfu所有keyLFU热点明显⭐⭐⭐⭐
volatile-lru有过期时间LRU部分持久化⭐⭐⭐
volatile-random有过期时间随机-
volatile-lfu有过期时间LFU-⭐⭐⭐
volatile-ttl有过期时间TTL最小有优先级⭐⭐⭐

💻 第三问:LRU vs LFU深入对比

LRU(最近最少使用)

场景演示:

时间线:
00:00 - 访问 key1
00:05 - 访问 key2
00:10 - 访问 key3
00:15 - 访问 key1  # key1最近被访问
00:20 - 内存满了

LRU链表:
key2(5分钟前) → key3(10分钟前) → key1(刚访问)
    ↑ 最久未访问
    
淘汰:key2

优点:

  • ✅ 简单有效
  • ✅ 保护热点数据

缺点:

  • ❌ 偶尔访问一次的key也会被保护
  • ❌ 不考虑访问频率

LFU(最不频繁使用)

场景演示:

访问记录:
key1 - 访问1000次(昨天)
key2 - 访问100次(今天)
key3 - 访问10次(刚才)

LFU统计:
key3(10次) → key2(100次) → key1(1000次)
    ↑ 访问最少
    
淘汰:key3

优点:

  • ✅ 考虑访问频率
  • ✅ 更准确识别热点

缺点:

  • ❌ 历史热点数据可能一直不被淘汰
  • ❌ 实现复杂

LRU vs LFU对比

阿西噶阿西: "我们用真实场景对比。"

场景: 新闻网站缓存

昨天热点新闻(访问10000次,今天没人看)
今天热点新闻(访问100次,刚发布)

LRU策略:
    淘汰昨天的新闻(最久未访问)✅
    保留今天的新闻 ✅

LFU策略:
    保留昨天的新闻(访问次数多)❌
    淘汰今天的新闻(访问次数少)❌

但Redis 4.0的LFU改进了:

LFU计数器会衰减:
    访问次数随时间衰减
    昨天的10000次 → 今天衰减到1000次
    今天的100次保持
    
淘汰昨天的新闻 ✅

⚙️ 第四问:实战配置

配置淘汰策略

# redis.conf

# 1. 设置最大内存
maxmemory 16gb

# 2. 设置淘汰策略
maxmemory-policy allkeys-lru

# 3. LRU样本数量(越大越精确,但越慢)
maxmemory-samples 5  # 默认5

运行时修改

# 查看当前配置
redis> CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

# 修改策略
redis> CONFIG SET maxmemory-policy allkeys-lru
OK

# 持久化配置
redis> CONFIG REWRITE
OK

监控内存使用

redis> INFO memory

# 关键指标:
used_memory:16106127360              # 已用内存(字节)
used_memory_human:15.00G             # 已用内存(可读)
used_memory_peak:16642998272         # 历史峰值
used_memory_peak_human:15.50G
maxmemory:17179869184                # 最大内存
maxmemory_human:16.00G
maxmemory_policy:allkeys-lru         # 淘汰策略
mem_fragmentation_ratio:1.05         # 内存碎片率

# 淘汰统计:
evicted_keys:12450                   # 已淘汰key数量

🔍 第五问:过期删除源码分析

惰性删除实现

南北绿豆: "我们看看源码是怎么实现的。"

// Redis源码(C语言,简化版)

// GET命令的实现
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
    
    // 查找key
    robj *o = lookupKeyRead(c->db, key);
    
    if (!o) {
        addReply(c, reply);
    }
    
    return o;
}

robj *lookupKeyRead(redisDb *db, robj *key) {
    
    // 查找key
    robj *val = lookupKey(db, key, LOOKUP_NONE);
    
    if (val == NULL) {
        return NULL;
    }
    
    // 检查是否过期
    if (expireIfNeeded(db, key) == 1) {
        return NULL;  // 已过期,删除后返回null
    }
    
    return val;
}

// 检查并删除过期key
int expireIfNeeded(redisDb *db, robj *key) {
    
    // 获取过期时间
    long long when = getExpire(db, key);
    
    if (when < 0) {
        return 0;  // 没有过期时间
    }
    
    // 检查是否过期
    if (mstime() <= when) {
        return 0;  // 未过期
    }
    
    // 已过期,删除key
    deleteKey(db, key);
    
    return 1;
}

流程:

客户端访问key
    ↓
lookupKey()查找
    ↓
expireIfNeeded()检查过期
    ↓
如果过期 → 删除 → 返回null
如果未过期 → 返回值

定期删除实现

// 定期删除函数(每100ms执行一次)
void activeExpireCycle(int type) {
    
    // 遍历所有数据库
    for (j = 0; j < dbs_per_call; j++) {
        
        int expired = 0;
        redisDb *db = server.db + (current_db % server.dbnum);
        
        do {
            unsigned long num, slots;
            long long now;
            
            // 1. 随机抽取20个key
            num = min(20, dictSize(db->expires));
            
            // 2. 遍历这些key
            while (num--) {
                dictEntry *de = dictGetRandomKey(db->expires);
                long long ttl = dictGetSignedIntegerVal(de) - now;
                
                // 3. 如果过期,删除
                if (ttl < 0) {
                    deleteKey(db, key);
                    expired++;
                }
            }
            
            // 4. 如果过期key超过25%,继续抽取
            // 单次执行时间不超过25ms
            
        } while (expired > 20 * 0.25 && !timelimitExceeded());
    }
}

🎨 第六问:实战案例

案例1:Session管理

@Service
public class SessionService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 创建Session
     */
    public String createSession(Long userId) {
        String sessionId = UUID.randomUUID().toString();
        String key = "session:" + sessionId;
        
        Map<String, String> sessionData = new HashMap<>();
        sessionData.put("userId", userId.toString());
        sessionData.put("createTime", String.valueOf(System.currentTimeMillis()));
        
        // 设置30分钟过期
        redisTemplate.opsForHash().putAll(key, sessionData);
        redisTemplate.expire(key, 30, TimeUnit.MINUTES);
        
        return sessionId;
    }
    
    /**
     * 验证Session(刷新过期时间)
     */
    public boolean validateSession(String sessionId) {
        String key = "session:" + sessionId;
        
        // 检查是否存在(惰性删除会在这里触发)
        Boolean exists = redisTemplate.hasKey(key);
        
        if (Boolean.TRUE.equals(exists)) {
            // Session有效,刷新过期时间(活跃用户续期)
            redisTemplate.expire(key, 30, TimeUnit.MINUTES);
            return true;
        }
        
        return false;
    }
}

案例2:验证码

@Service
public class SmsService {
    
    /**
     * 发送验证码
     */
    public void sendCode(String phone) {
        String code = generateCode();  // 生成6位数字
        String key = "code:" + phone;
        
        // 验证码5分钟过期(定期删除 + 惰性删除)
        redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);
        
        // 发送短信...
        System.out.println("发送验证码:" + code + " 到 " + phone);
    }
    
    /**
     * 验证码校验
     */
    public boolean verifyCode(String phone, String code) {
        String key = "code:" + phone;
        
        // 获取验证码(惰性删除:过期会自动删除)
        String savedCode = redisTemplate.opsForValue().get(key);
        
        if (savedCode == null) {
            System.out.println("验证码不存在或已过期");
            return false;
        }
        
        if (savedCode.equals(code)) {
            // 验证成功,立即删除(防止重复使用)
            redisTemplate.delete(key);
            return true;
        }
        
        return false;
    }
}

案例3:热点数据缓存(配置LRU)

@Service
public class ProductService {
    
    /**
     * 获取商品(LRU自动淘汰冷数据)
     */
    public Product getProduct(Long productId) {
        String key = "product:" + productId;
        
        // 查询Redis
        String json = redisTemplate.opsForValue().get(key);
        if (json != null) {
            // 访问了这个key,LRU会更新其访问时间
            return JSON.parseObject(json, Product.class);
        }
        
        // 查询数据库
        Product product = productDao.findById(productId);
        
        if (product != null) {
            // 缓存(不设置过期时间,让LRU自动淘汰)
            redisTemplate.opsForValue().set(key, JSON.toJSONString(product));
        }
        
        return product;
    }
}

效果:

商品访问情况:
iPhone(热点) → 访问10000次/天 → 一直在缓存中 ✅
普通商品 → 访问10次/天 → 被LRU淘汰 ✅
冷门商品 → 访问1次/月 → 立即被淘汰 ✅

内存始终存储的是热点数据!

💡 第七问:最佳实践

1. 策略选择指南

决策树:

你的场景是?
│
├─ 数据绝对不能丢
│  └─ noeviction(不推荐,提前扩容)
│
├─ 所有数据访问频率相近
│  └─ allkeys-random
│
├─ 明显的热点数据
│  ├─ 近期热点 → allkeys-lru(推荐)
│  └─ 长期热点 → allkeys-lfu
│
├─ 部分数据需要持久化
│  └─ volatile-lru
│
└─ 不确定
   └─ allkeys-lru(默认推荐)

2. 内存使用优化

优化1:合理设置过期时间

// ❌ 不推荐:不设置过期时间
redisTemplate.opsForValue().set(key, value);

// ❌ 不推荐:过期时间太长
redisTemplate.opsForValue().set(key, value, 7, TimeUnit.DAYS);

// ✅ 推荐:根据业务设置合理的过期时间
redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);

优化2:避免大Key

// ❌ 不推荐:一个key存储大量数据
String json = JSON.toJSONString(largeObject);  // 10MB
redisTemplate.opsForValue().set("large:key", json);

// ✅ 推荐:拆分成多个小key
Map<String, String> map = convertToMap(largeObject);
for (Map.Entry<String, String> entry : map.entrySet()) {
    redisTemplate.opsForValue().set("object:" + entry.getKey(), entry.getValue());
}

// ✅ 或使用Hash
redisTemplate.opsForHash().putAll("object:123", map);

优化3:定期清理无用数据

@Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点
public void cleanupExpiredData() {
    
    // 清理7天前的日志
    String pattern = "log:*";
    Set<String> keys = redisTemplate.keys(pattern);
    
    long sevenDaysAgo = System.currentTimeMillis() - 7 * 24 * 3600 * 1000;
    
    for (String key : keys) {
        String timestamp = key.split(":")[1];
        if (Long.parseLong(timestamp) < sevenDaysAgo) {
            redisTemplate.delete(key);
        }
    }
}

3. 内存告警

@Component
public class RedisMemoryMonitor {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Scheduled(fixedRate = 60000)  // 每分钟检查
    public void checkMemory() {
        
        Properties info = redisTemplate.execute((RedisCallback<Properties>) connection -> 
            connection.info("memory"));
        
        String usedMemory = info.getProperty("used_memory");
        String maxMemory = info.getProperty("maxmemory");
        
        if (maxMemory != null && !maxMemory.equals("0")) {
            long used = Long.parseLong(usedMemory);
            long max = Long.parseLong(maxMemory);
            
            double usage = (double) used / max * 100;
            
            // 内存使用超过80%,告警
            if (usage > 80) {
                System.err.println("⚠️  Redis内存告警:使用率 " + 
                    String.format("%.2f", usage) + "%");
                
                // 发送告警通知...
            }
        }
    }
}

🐛 第八问:常见问题

问题1:SET命令覆盖过期时间

错误示例:

// 初始化:设置1小时过期
redisTemplate.opsForValue().set("user:1", "data", 1, TimeUnit.HOURS);

// 更新:忘了设置过期时间
redisTemplate.opsForValue().set("user:1", "newData");  // ← 过期时间没了!

// 查看TTL
redis> TTL user:1
(integer) -1  # 永不过期了

正确做法:

// 方案1:每次都设置过期时间
redisTemplate.opsForValue().set("user:1", "newData", 1, TimeUnit.HOURS);

// 方案2:先获取剩余TTL,再设置
Long ttl = redisTemplate.getExpire("user:1", TimeUnit.SECONDS);
if (ttl > 0) {
    redisTemplate.opsForValue().set("user:1", "newData", ttl, TimeUnit.SECONDS);
} else {
    redisTemplate.opsForValue().set("user:1", "newData", 3600, TimeUnit.SECONDS);
}

// 方案3:使用GETSET(不影响过期时间)
redisTemplate.opsForValue().getAndSet("user:1", "newData");

问题2:大量key同时过期

场景:

// 批量导入商品,都设置1小时过期
for (Product product : products) {
    String key = "product:" + product.getId();
    redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 
        3600, TimeUnit.SECONDS);  // 都是3600秒
}

// 1小时后,所有key同时过期
// 定期删除任务压力大
// 可能导致Redis卡顿

解决方案:

// 添加随机过期时间
for (Product product : products) {
    String key = "product:" + product.getId();
    
    // 1小时 + 0-5分钟随机
    int expire = 3600 + new Random().nextInt(300);
    
    redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 
        expire, TimeUnit.SECONDS);
}

// key过期时间分散,避免同时过期

问题3:内存满了怎么办?

紧急处理:

# 1. 查看内存使用
redis> INFO memory

# 2. 查看key数量
redis> DBSIZE

# 3. 查找大key
redis-cli --bigkeys

# 输出:
Biggest string found: 'large:key' has 10485760 bytes
Biggest list found: 'large:list' has 100000 items
Biggest hash found: 'large:hash' has 50000 fields

# 4. 删除大key或无用key
redis> DEL large:key

# 5. 临时提高maxmemory
redis> CONFIG SET maxmemory 20gb

# 6. 修改淘汰策略
redis> CONFIG SET maxmemory-policy allkeys-lru

长期方案:

1. 扩容
   - 增加内存
   - 或使用Cluster分片

2. 优化数据
   - 删除无用数据
   - 压缩数据
   - 拆分大key

3. 优化过期时间
   - 设置合理的过期时间
   - 定期清理

4. 配置淘汰策略
   - 选择合适的策略

💡 知识点总结

过期删除与内存淘汰核心要点

过期删除策略

  • 惰性删除:访问时删除
  • 定期删除:后台定期清理
  • Redis使用:惰性 + 定期

内存淘汰策略(8种)

  • noeviction:不淘汰(默认)
  • allkeys-lru:LRU淘汰(推荐)
  • allkeys-lfu:LFU淘汰
  • allkeys-random:随机淘汰
  • volatile-lru:有过期时间的LRU
  • volatile-lfu:有过期时间的LFU
  • volatile-random:有过期时间的随机
  • volatile-ttl:TTL最小的先淘汰

LRU vs LFU

  • LRU:最近最少使用(考虑时间)
  • LFU:最不频繁使用(考虑频率)
  • LRU适合通用场景
  • LFU适合明显热点场景

最佳实践

  • 配置maxmemory
  • 选择合适的淘汰策略(allkeys-lru)
  • 设置合理的过期时间
  • 添加随机过期时间
  • 监控内存使用

常见问题

  • SET覆盖过期时间
  • 大量key同时过期
  • 内存满了的处理

记忆口诀

过期删除两策略,
惰性定期来配合。
访问时刻做检查,
后台定期抽样删。
内存满了要淘汰,
八种策略供选择。
allkeys-lru最常用,
最近最少先淘汰。
LFU考虑访问频,
热点数据保留住。
合理设置过期时,
随机时间防雪崩。

🤔 常见面试题

Q1: Redis如何删除过期key?

A:

两种策略结合:

1. 惰性删除(被动)
   - 访问key时检查是否过期
   - 过期就删除
   - CPU友好,内存不友好

2. 定期删除(主动)
   - 每100ms执行一次
   - 随机抽取20个key检查
   - 过期超过25%继续抽取
   - 单次不超过25ms
   - 平衡CPU和内存

仅靠这两种还不够,内存可能满
所以还需要内存淘汰策略

Q2: Redis内存淘汰策略有哪些?

A:

8种策略:

不淘汰:
- noeviction

所有key:
- allkeys-lru(推荐)
- allkeys-lfu
- allkeys-random

有过期时间的key:
- volatile-lru
- volatile-lfu
- volatile-random
- volatile-ttl

推荐:allkeys-lru
- 适用99%场景
- 淘汰最久未访问的key

Q3: LRU和LFU的区别?

A:

LRU(Least Recently Used):
- 淘汰最近最少使用的
- 只看访问时间
- 最久没访问的先淘汰

示例:
key1: 10分钟前访问(淘汰)
key2: 5分钟前访问
key3: 1分钟前访问

LFU(Least Frequently Used):
- 淘汰最不频繁使用的
- 看访问频率
- 访问次数最少的先淘汰

示例:
key1: 访问1000次
key2: 访问100次
key3: 访问10次(淘汰)

选择:
- 缓存系统:LRU(访问模式变化快)
- 热榜系统:LFU(长期热点明显)

💬 写在最后

从过期删除到内存淘汰,我们深入学习了Redis的内存管理:

  • ⏰ 理解了过期删除的两种策略
  • 🔥 掌握了8种内存淘汰策略
  • 📊 对比了LRU和LFU算法
  • 💻 完成了实战案例

这篇文章,希望能让你的Redis内存使用更加高效!

如果这篇文章对你有帮助,请:

  • 👍 点赞支持
  • ⭐ 收藏备用
  • 🔄 转发分享
  • 💬 评论交流

感谢阅读,期待下次再见! 👋