难度:⭐⭐⭐⭐ | 适合人群:想优化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:1 → 10分钟前访问
user:2 → 5分钟前访问
user:3 → 1分钟前访问
淘汰顺序:
user:1(最久未访问)→ user:2 → user:3
适用: 通用场景(推荐)
3. allkeys-random
从所有key中
↓
随机淘汰
适用: 访问频率相近的场景
4. allkeys-lfu(Redis 4.0+)
从所有key中
↓
使用LFU算法(Least Frequently Used)
↓
淘汰访问频率最低的key
LFU算法:
key访问次数:
user:1 → 访问1000次
user:2 → 访问100次
user:3 → 访问10次
淘汰顺序:
user:3(访问最少)→ user:2 → user: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 | 所有key | LRU | 通用场景 | ⭐⭐⭐⭐⭐ |
| allkeys-random | 所有key | 随机 | 访问频率相近 | ⭐⭐ |
| allkeys-lfu | 所有key | LFU | 热点明显 | ⭐⭐⭐⭐ |
| 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内存使用更加高效!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流
感谢阅读,期待下次再见! 👋