Redis 核心知识笔记 - 第六部分:内存过期与淘汰策略
Redis 核心知识笔记 - 第六部分:内存过期与淘汰策略
一、过期策略
1.1 过期时间设置
Redis 允许为 Key 设置过期时间(TTL),到期后自动删除。
# 设置过期时间
EXPIRE key seconds # 设置秒级过期时间
PEXPIRE key milliseconds # 设置毫秒级过期时间
EXPIREAT key timestamp # 设置过期时间点(Unix 时间戳,秒)
PEXPIREAT key timestamp # 设置过期时间点(Unix 时间戳,毫秒)
# 设置值的同时设置过期时间
SET key value EX seconds # 秒
SET key value PX milliseconds # 毫秒
SETEX key seconds value # 秒(旧语法)
# 查看剩余过期时间
TTL key # 返回秒数,-1 表示永不过期,-2 表示不存在
PTTL key # 返回毫秒数
# 移除过期时间
PERSIST key # 移除过期时间,变为永不过期
1.2 过期删除策略
Redis 使用 惰性删除 + 定期删除 两种策略配合:
惰性删除(Lazy Expiration)
优点:
- CPU 友好,只在访问时检查
- 不会浪费 CPU 检查未访问的 Key
缺点:
- 内存不友好,过期但未访问的 Key 会一直占用内存
定期删除(Periodic Expiration)
配置参数:
# redis.conf
# 定期删除频率,默认 10(每秒检查10次)
hz 10
1.3 过期策略对比
| 策略 | 触发时机 | 优点 | 缺点 |
|---|---|---|---|
| 定时删除 | 到期立即删除 | 内存友好 | CPU 开销大(Redis 未采用) |
| 惰性删除 | 访问时检查 | CPU 友好 | 可能内存泄漏 |
| 定期删除 | 周期性检查 | 折中方案 | 仍可能有过期未删除的 Key |
Note
Redis 同时使用惰性删除和定期删除,但仍可能存在过期 Key 未及时删除的情况,这时就需要内存淘汰策略来保证内存不会被撑满。
二、内存淘汰策略
2.1 什么时候触发内存淘汰
当 Redis 使用内存达到 maxmemory 配置时,会触发内存淘汰。
# redis.conf
# 设置最大内存限制
maxmemory 4gb
# 设置淘汰策略
maxmemory-policy allkeys-lru
# 查看内存使用情况
INFO memory
2.2 八种淘汰策略
2.3 策略详解
| 策略 | 范围 | 算法 | 说明 |
|---|---|---|---|
| noeviction | - | - | 不淘汰,写入时返回 OOM 错误 |
| allkeys-lru | 所有 Key | LRU | 淘汰最近最少使用的 Key |
| allkeys-lfu | 所有 Key | LFU | 淘汰使用频率最低的 Key |
| allkeys-random | 所有 Key | 随机 | 随机淘汰 Key |
| volatile-lru | 有过期时间的 Key | LRU | 淘汰最近最少使用的 Key |
| volatile-lfu | 有过期时间的 Key | LFU | 淘汰使用频率最低的 Key |
| volatile-random | 有过期时间的 Key | 随机 | 随机淘汰 Key |
| volatile-ttl | 有过期时间的 Key | TTL | 优先淘汰即将过期的 Key |
2.4 LRU vs LFU
LRU(Least Recently Used)
- 原理:根据最后访问时间排序,淘汰最久未访问的
- 问题:无法区分访问频率,冷数据偶尔被访问会导致热数据被淘汰
Key A: 被访问 1000 次,最后访问时间 10:00
Key B: 被访问 1 次,最后访问时间 10:01
LRU 会保留 Key B(更近访问),淘汰 Key A(实际更重要)
LFU(Least Frequently Used)
- 原理:根据访问频率排序,淘汰访问次数最少的
- 优势:更准确识别热点数据
- 注意:Redis 4.0+ 支持
Key A: 被访问 1000 次
Key B: 被访问 1 次
LFU 会保留 Key A,淘汰 Key B ✓
2.5 Redis 近似 LRU 实现
Redis 没有使用传统的 LRU 链表(开销太大),而是使用近似 LRU 算法:
# redis.conf
# LRU 采样数量,越大越精确,但 CPU 开销越大
maxmemory-samples 5
2.6 LFU 参数调优
# redis.conf
# LFU 计数器衰减因子(0-255)
# 值越大,衰减越慢,需要更多时间不访问才会降低频率
lfu-decay-time 1
# LFU 对数因子
# 值越大,达到最大频率所需的访问次数越多
lfu-log-factor 10
LFU 计数器增长曲线:
| lfu-log-factor | 达到 255 所需访问次数 |
|---|---|
| 0 | 255 |
| 1 | 524 |
| 10 | 10000 |
| 100 | 100万 |
三、策略选择建议
3.1 决策流程
3.2 场景推荐
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 通用缓存 | allkeys-lru | 简单有效,自动淘汰冷数据 |
| 热点数据明显 | allkeys-lfu | 更好保留热点数据 |
| 部分数据永不过期 | volatile-lru | 只淘汰有过期时间的 Key |
| Session 缓存 | volatile-ttl | 优先删除即将过期的会话 |
| 数据一致性要求高 | noeviction | 宁可报错也不丢数据 |
四、Java 代码示例
4.1 设置过期时间
@Service
public class RedisExpireService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 设置带过期时间的缓存
*/
public void setWithExpire(String key, String value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 设置过期时间
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 设置过期时间点
*/
public Boolean expireAt(String key, Date date) {
return redisTemplate.expireAt(key, date);
}
/**
* 获取剩余过期时间
*/
public Long getExpire(String key, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}
/**
* 移除过期时间
*/
public Boolean persist(String key) {
return redisTemplate.persist(key);
}
/**
* 续期示例:用于分布式锁续期
*/
public Boolean renewExpire(String key, String expectedValue, long timeout, TimeUnit unit) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('pexpire', KEYS[1], ARGV[2]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
expectedValue,
String.valueOf(unit.toMillis(timeout))
);
return result != null && result == 1;
}
}
4.2 内存监控
@Service
public class RedisMemoryService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 获取内存信息
*/
public Map<String, Object> getMemoryInfo() {
Properties info = redisTemplate.execute((RedisCallback<Properties>)
connection -> connection.info("memory")
);
Map<String, Object> result = new HashMap<>();
if (info != null) {
// 已使用内存
result.put("used_memory", info.getProperty("used_memory"));
result.put("used_memory_human", info.getProperty("used_memory_human"));
// 峰值内存
result.put("used_memory_peak", info.getProperty("used_memory_peak"));
result.put("used_memory_peak_human", info.getProperty("used_memory_peak_human"));
// 最大内存配置
result.put("maxmemory", info.getProperty("maxmemory"));
result.put("maxmemory_human", info.getProperty("maxmemory_human"));
// 淘汰策略
result.put("maxmemory_policy", info.getProperty("maxmemory_policy"));
// 内存碎片率
result.put("mem_fragmentation_ratio", info.getProperty("mem_fragmentation_ratio"));
}
return result;
}
/**
* 获取 Key 占用内存大小(Redis 4.0+)
*/
public Long getKeyMemoryUsage(String key) {
return redisTemplate.execute((RedisCallback<Long>) connection -> {
// MEMORY USAGE key
Object result = connection.execute("MEMORY",
"USAGE".getBytes(),
key.getBytes()
);
return result != null ? (Long) result : null;
});
}
/**
* 获取淘汰统计信息
*/
public Map<String, Object> getEvictionStats() {
Properties info = redisTemplate.execute((RedisCallback<Properties>)
connection -> connection.info("stats")
);
Map<String, Object> result = new HashMap<>();
if (info != null) {
// 被淘汰的 Key 数量
result.put("evicted_keys", info.getProperty("evicted_keys"));
// 过期的 Key 数量
result.put("expired_keys", info.getProperty("expired_keys"));
// 缓存命中率
String hits = info.getProperty("keyspace_hits");
String misses = info.getProperty("keyspace_misses");
if (hits != null && misses != null) {
long h = Long.parseLong(hits);
long m = Long.parseLong(misses);
double hitRate = h * 100.0 / (h + m);
result.put("hit_rate", String.format("%.2f%%", hitRate));
}
}
return result;
}
}
4.3 智能过期时间
@Service
public class SmartCacheService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final Random random = new Random();
/**
* 设置缓存,带随机过期时间(防止缓存雪崩)
*/
public void setWithRandomExpire(String key, String value,
long baseSeconds, long randomRange) {
long expire = baseSeconds + random.nextInt((int) randomRange);
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
}
/**
* 缓存预热:批量设置不同过期时间
*/
public void warmupCache(Map<String, String> data, long baseSeconds) {
data.forEach((key, value) -> {
// 过期时间在 baseSeconds 的 80%-120% 之间随机
long expire = (long) (baseSeconds * (0.8 + random.nextDouble() * 0.4));
redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
});
}
/**
* 访问时续期(延长热点数据生命周期)
*/
public String getAndRefresh(String key, long refreshThreshold,
long newExpire, TimeUnit unit) {
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
// 检查剩余过期时间
Long ttl = redisTemplate.getExpire(key, unit);
if (ttl != null && ttl < refreshThreshold) {
// 剩余时间小于阈值,续期
redisTemplate.expire(key, newExpire, unit);
}
}
return value;
}
}
五、运维监控
5.1 关键监控指标
# 查看内存使用
redis-cli INFO memory
# 关键指标
used_memory:1073741824 # 已使用内存(字节)
used_memory_human:1.00G # 已使用内存(可读)
used_memory_rss:1181116416 # 操作系统分配的内存
used_memory_peak:1073741824 # 历史峰值
maxmemory:2147483648 # 最大内存配置
maxmemory_policy:allkeys-lru # 淘汰策略
mem_fragmentation_ratio:1.10 # 内存碎片率
5.2 内存碎片处理
# 内存碎片率 > 1.5 时需要关注
# 方法1:重启 Redis(简单粗暴)
# 方法2:开启主动碎片整理(Redis 4.0+)
CONFIG SET activedefrag yes
# 碎片整理配置
active-defrag-ignore-bytes 100mb # 碎片达到多少才开始整理
active-defrag-threshold-lower 10 # 碎片率低于此值不整理
active-defrag-threshold-upper 100 # 碎片率超过此值全力整理
active-defrag-cycle-min 1 # 整理周期最小 CPU 占用
active-defrag-cycle-max 25 # 整理周期最大 CPU 占用
5.3 过期 Key 清理脚本
#!/bin/bash
# 批量删除过期 Key(适用于 Key 太多导致定期删除不及时的情况)
REDIS_CLI="redis-cli -h 127.0.0.1 -p 6379 -a password"
PATTERN="cache:*"
COUNT=100
# 使用 SCAN 遍历,避免 KEYS 命令阻塞
cursor=0
deleted=0
while true; do
result=$($REDIS_CLI SCAN $cursor MATCH "$PATTERN" COUNT $COUNT)
cursor=$(echo "$result" | head -n 1)
keys=$(echo "$result" | tail -n +2)
for key in $keys; do
# 检查 Key 是否已过期(TTL = -2 表示不存在/已过期)
ttl=$($REDIS_CLI TTL "$key")
if [ "$ttl" = "-2" ]; then
$REDIS_CLI DEL "$key"
((deleted++))
fi
done
if [ "$cursor" = "0" ]; then
break
fi
done
echo "Deleted $deleted expired keys"
六、最佳实践
6.1 过期时间设置原则
| 原则 | 说明 |
|---|---|
| 必须设置 TTL | 避免内存泄漏,除非数据确实永不过期 |
| 避免集中过期 | 随机化过期时间,防止缓存雪崩 |
| 热点数据续期 | 频繁访问的数据可延长过期时间 |
| 合理设置时长 | 太短增加 DB 压力,太长浪费内存 |
6.2 淘汰策略配置建议
# 生产环境推荐配置
# 设置最大内存(通常为物理内存的 70-80%)
maxmemory 12gb
# 推荐使用 allkeys-lru 或 allkeys-lfu
maxmemory-policy allkeys-lfu
# 增加采样数量提高精确度
maxmemory-samples 10
# 开启惰性释放(Redis 4.0+)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
6.3 内存使用优化
- 使用合适的数据结构:Hash 存储对象比 String 更节省内存
- 压缩 Value:对大 Value 进行 GZIP 压缩
- 精简 Key 名:使用简短但有意义的 Key 前缀
- 使用整数代替字符串:整数可以使用更紧凑的编码
- 定期清理无用 Key:使用 SCAN 定期扫描清理