在前面的文章中,我们聊了分布式锁、聊了持久化,这些都是在讲"怎么用好 Redis"。
但今天我们要聊一个更底层、更残酷的话题:资源限制与系统脆弱性。
Redis 再快,它也是基于内存的。内存是昂贵的资源,不可能无限扩容。同时,Redis 作为数据库的挡箭牌,一旦它出了问题,数据库瞬间就会面临灭顶之灾。
今天这篇,我们就来把 Redis 的 内存淘汰策略 和 缓存击穿/穿透/雪崩 彻底讲透。
一、 内存满了怎么办?(内存淘汰策略)
Redis 的性能虽然强悍,但它不是黑洞。在生产环境中,我们通常会在配置文件中设置 maxmemory 来限制 Redis 的最大内存使用量。
那么问题来了:当数据量存满了,新数据又要塞进来,Redis 该扔掉谁?
Redis 提供了 8 种策略,看起来眼花缭乱。其实作为博主,我建议你不要死记硬背,而是从 "范围" 和 "算法" 两个维度去理解。
1. 维度一:你要对谁下手?(筛选范围)
Redis 并不总是对所有数据都"赶尽杀绝",它把数据分为了两类圈子:
-
圈子一:Volatile(临时工区 - 仅针对设置了 TTL 的 Key)
-
含义 :Redis 只会从设置了过期时间的 Key 中进行淘汰。
-
场景 :这非常适合 "混合存储" 场景。比如你的 Redis 里既存了必须持久化的重要配置(没设 TTL),又存了大量的临时缓存(设了 TTL)。当内存不足时,Redis 会很懂事地只清理那些本来就会过期的缓存,绝对不会碰你的重要数据。
-
-
圈子二:Allkeys(全员区 - 针对所有 Key)
-
含义 :不管你设没设过期时间,一视同仁,大家都在被淘汰的范围内。
-
场景 :这是 "纯缓存" 模式的最佳选择。因为作为缓存,只要内存满了,没用的数据就该腾位置,不需要保留任何特权阶级。
-
2. 维度二:你要怎么挑?(筛选算法)
-
LRU (Least Recently Used) :淘汰最久没被使用过的。
- 逻辑:你虽然进来的早,但最近 3 个月都没人查过你,留着浪费资源,请你走人。
-
LFU (Least Frequently Used) :淘汰使用频率最低的。
- 逻辑:虽然你刚才被访问了一次,但你是这一年来唯一的访问,说明你不够热,请你走人。
-
Random :随机。闭着眼瞎选一个删掉。
-
TTL :剩余寿命最短。既然你马上就要过期了,不如我提前送你一程(仅适用于 Volatile 范围)。
3. 策略大盘点与选型建议
将上述两个维度组合,再加上默认策略,就构成了 8 种策略:
| 策略名 | 筛选范围 | 淘汰规则 | 博主推荐场景 |
|---|---|---|---|
| noeviction | - | 不删除,直接报错 | (默认配置) 适用于数据绝不能丢的场景,需配合完善的扩容监控。 |
| volatile-lru | 有TTL的 | 最久未用 | 适用于 Redis 既做缓存又做存储 的混合场景。 |
| volatile-lfu | 有TTL的 | 最少频率 | 同上。 |
| volatile-random | 有TTL的 | 随机删除 | 极其少用。 |
| volatile-ttl | 有TTL的 | 即将过期 | 哪怕没过期,只要快到了就删,不如 LRU 智能。 |
| allkeys-lru | 所有Key | 最久未用 | 最常用! 适用于通用的缓存场景,保留热点数据。 |
| allkeys-lfu | 所有Key | 最少频率 | 适用于有些生僻数据很久才被访问一次的场景。 |
| allkeys-random | 所有Key | 随机删除 | 极其少用。 |

说明:
-
绝大多数业务场景,直接闭眼选
allkeys-lru。 -
因为缓存的本质就是"把热的数据留下",LRU 是最符合这个直觉的算法。
二、 缓存的三大"天坑":击穿、穿透、雪崩
Redis 就像一道堤坝,挡在脆弱的 MySQL 数据库前面。如果这道堤坝出了问题,洪水(高并发请求)就会直接冲垮数据库。
我们按照问题的**"刁钻程度"**,由点到面来分析。
1. 缓存击穿 (Cache Breakdown) ------ "单点爆破"
简单来说就是热点key过期的瞬间,大量请求达到了DB。
这是最常见,但也最需要技术含量的场景。
-
现象:
某个 超级热点 Key(比如微博热搜、秒杀商品),并发量极高。在它 TTL 过期的一瞬间,几万个请求同时发现 Redis 没数据,同时冲向数据库去构建缓存。
结果: 数据库瞬间被打死。
-
解决方案:
针对这个问题,我们有两种截然不同的思路。
方案 A:互斥锁 (Consistency First)
既然大家都要去查数据库,那就排队!
-
思路:只允许一个线程去查询数据库并重建缓存,其他线程阻塞等待。
-
流程:
-
线程 1 发现缓存没数据。
-
线程 1 获取分布式锁(Mutex Lock)。
-
线程 1 去查库、写缓存、释放锁。
-
线程 2、3、4 在获取锁失败后,阻塞等待,或者休眠一会再重试(Double Check)。
-
-
优点:数据一致性高。
-
缺点:性能有损耗,大家都在等那一个人。
方案 B:永不过期 + 逻辑过期 (Availability First)(就好像他本应该过期的,但是我留下了他用来保护数据库,反正我先返回给你,然后更新重置缓存)
这个方案牺牲了一点点数据一致性,换取了极致的性能。这也是很多高并发系统的首选。
-
核心思路:
-
Redis 层面 :Key 不设置 TTL,所以物理上它永远不会过期(除非被淘汰)。
-
业务层面 :我们在 Value 的内容里,多存一个属性:
expireTime(逻辑过期时间)。
-
-
具体实现流程:
-
查询缓存:请求命中缓存后,读取 Value。
-
判断过期 :检查 Value 里的
expireTime字段,看是否已过期。 -
未过期:直接返回数据,结束。
-
已过期:
-
获取锁:尝试获取互斥锁。
-
开启异步线程 :如果获取锁成功,说明我是"被选中"的那个人。我立马开启一个独立的异步线程 去查询数据库、更新缓存(重置
expireTime)、释放锁。 -
返回旧值 :无论是获取锁成功的当前线程,还是获取锁失败的其他线程,都不用等,直接把手里这份"已经过期"的旧数据返回给用户。
-
-
-
说明:
这种方案非常巧妙。它实际上是"悄悄地在后台更新"。用户在更新完成前的几百毫秒内,虽然看到的是旧数据,但系统响应速度极快,没有任何阻塞。
-
2. 缓存穿透 (Cache Penetration) ------ "打在了空气上"
关键在⼀个透字,透过去了,即数据在 Redis 缓存和数据库中都不存在这样缓存永远 不会生效,请求都会打到数据库。(说白了,没得给你缓存的机会)
如果说击穿是"热点失效",那穿透就是"恶意攻击"。
-
现象:
请求想要的数据,Redis 里没有,数据库里也没有。
这样 Redis 就成了摆设,请求每次都直通数据库。这通常是代码 bug 或者黑客恶意攻击(一直请求 ID=-1 的数据)。
-
解决方案:
-
缓存空对象 :数据库查不到,我也在 Redis 存一个
null,并设个短一点的过期时间。下次你再来查 -1,Redis 直接告诉你"是空",别烦数据库。 -
布隆过滤器 (Bloom Filter):在访问 Redis 之前加一道屏障。布隆过滤器能高效地判断"这个 ID 是否存在"。如果布隆说不存在,直接打回,连 Redis 都不用查。
-
布隆过滤器原理分析:
如果说缓存空值是被动防御,那布隆过滤器就是主动拦截。
-
核心原理 : 底层是一个巨大的 BitMap (位图) + N 个哈希函数。
-
存入时 :经过 N 次 Hash 计算,将对应位置的比特位全部置为
1。 -
查询时:同样的 N 次 Hash 计算,去查看对应位置。
-
-
判定规则(铁律):
-
只要有一位是 0 :说明该 Key 绝对不存在 。 -> 直接拦截,无需查库。
-
如果全都是 1 :说明该 Key 可能存在 (存在极低概率是别的 Key 凑出来的 Hash 冲突)。 -> 放行。
-
3. 缓存雪崩 (Cache Avalanche) ------ "全线崩塌"
同⼀时间⼤量key过期,导致查询全打到数据库上
最后一种情况,是规模最大的灾难。
-
现象:
大量的 Key 在同一时间集体过期,或者 Redis 节点直接宕机。
这导致此刻所有的请求全部涌向数据库,数据库瞬间压力过大而崩溃。
-
解决方案:
-
打散过期时间 :在设置 TTL 时,不要设成固定的 1 小时,而是 TTL + 随机数(1-5分钟)。让 Key 排队过期,不要挤在一起。
-
高可用架构:利用 Redis Sentinel 或 Cluster,保证 Redis 挂了一个节点,其他的能立马顶上。
-
服务降级:当缓存和数据库都扛不住时,直接返回默认值或错误提示,保住系统不崩。
-

总结
Redis 的学习,不能只停留在命令的使用上。
-
内存淘汰 是为了让 Redis 永远只留最有价值的数据。
-
击穿/穿透/雪崩 的防御,是为了让 Redis 在面对流量洪峰和异常情况时,依然能稳稳地守护身后的数据库。
掌握了这些,你才算真正具备了驾驭大规模 Redis 集群的能力。下一篇,我们将深入 Redis 的 集群架构,敬请期待!