一、Redis 过期删除策略:惰性删除 + 定期删除
Redis 中设置 key 过期时间非常简单:
# 设置 60 秒后过期EXPIRE session:user:100160# 设置具体过期时间戳EXPIREAT session:user:10011699123456# 查看剩余 TTLTTL session:user:1001但 key 过期后,Redis 并不会立即将其从内存中删除。Redis 采用了**惰性删除(Lazy Expiry)和定期删除(Periodic Expiry)**两种策略的组合机制。
1.1 惰性删除(Lazy Expiry)
核心思想:当客户端访问某个 key 时,Redis 才会检查该 key 是否已过期,如果过期则立即删除。
执行流程:
客户端发起 GET key ↓ 检查 key 是否在过期字典中 ↓ 比较当前时间与过期时间戳 ↓ 若已过期 → 删除 key 并返回 nil 若未过期 → 正常返回数据优点:
- CPU 友好:只在访问时检查,不消耗额外 CPU 资源
- 实现简单:无需后台线程轮询
缺点:
- 内存不友好:如果大量过期 key 从未被访问,将长期占用内存,形成"内存泄漏"
- 存在过期数据:在未被访问前,过期数据仍存在于内存中
1.2 定期删除(Periodic Expiry)
核心思想:Redis 后台线程每隔一段时间(默认 100ms),随机抽取一批 key 检查是否过期,删除其中已过期的 key。
执行流程:
每 100ms 执行一次扫描 ↓ 随机抽取 20 个设置了过期时间的 key ↓ 删除其中已过期的 key ↓ 如果过期比例 > 25% ↓ 继续扫描,最多持续 25ms(避免阻塞主线程)优点:
- 内存友好:主动清理过期 key,防止内存泄漏
- 可控:通过限制单次扫描时间和比例,避免阻塞主线程
缺点:
- 频率难控制:扫描太频繁消耗 CPU,扫描太少导致内存泄漏
- 存在漏网之鱼:如果过期 key 比例极高,25ms 内无法全部清理
1.3 为什么 Redis 不采用定时删除?
定时删除(为每个过期 key 设置一个定时器,到期立即删除)听起来很理想,但 Redis 没有选择它,原因有二:
- CPU 开销大:需要维护大量定时器,消耗大量 CPU 资源
- 实现复杂:需要精确的时间管理,增加系统复杂度
Redis 的设计哲学:在内存和 CPU 之间做权衡,用"组合策略"达到最优平衡。
1.4 生产环境建议
# 查看过期 key 的内存占用INFO keyspace# 避免大量 key 同时过期(设置随机偏移量)EXPIRE key$((RANDOM%3600+3600))# 1~2 小时随机过期二、Redis 内存淘汰策略:8 种策略全解析
当 Redis 内存达到maxmemory上限时,需要淘汰部分 key 来释放空间。Redis 提供了8 种内存淘汰策略:
2.1 策略分类一览
| 策略 | 类型 | 说明 |
|---|---|---|
| noeviction | 不淘汰 | 内存满时拒绝写入,返回错误 |
| allkeys-random | 随机淘汰 | 从所有 key 中随机淘汰 |
| volatile-random | 随机淘汰 | 从设置了 TTL 的 key 中随机淘汰 |
| volatile-ttl | TTL 优先 | 优先淘汰即将过期的 key |
| volatile-lru | LRU 淘汰 | 从设置了 TTL 的 key 中淘汰最久未使用的 |
| allkeys-lru | LRU 淘汰 | 从所有 key 中淘汰最久未使用的 |
| volatile-lfu | LFU 淘汰 | 从设置了 TTL 的 key 中淘汰使用频率最低的 |
| allkeys-lfu | LFU 淘汰 | 从所有 key 中淘汰使用频率最低的 |
2.2 配置方法
# redis.confmaxmemory 256mb maxmemory-policy allkeys-lru# 动态修改(无需重启)CONFIG SET maxmemory-policy allkeys-lfu2.3 生产环境选型建议
| 场景 | 推荐策略 | 理由 |
|---|---|---|
| 纯缓存场景 | allkeys-lru | 最常用,保留热数据,淘汰冷数据 |
| 缓存 + 持久化混合 | volatile-lru | 保护无 TTL 的重要数据不被淘汰 |
| 有明显冷热区分 | allkeys-lfu | 比 LRU 更精准识别真正的热 key |
| 避免使用 | random/ttl | 淘汰策略不可控,可能误淘汰热数据 |
2.4 LRU vs LFU:内存淘汰策略的进化
LRU(Least Recently Used):
- 核心思想:淘汰最久未被访问的 key
- Redis 实现:记录 24bit 的访问时间戳,采用近似 LRU(随机采样 5 个,淘汰最久未使用的)
- 问题:如果某个 key 被大量访问后不再使用,它仍会在 LRU 链表中占据靠前位置,占用宝贵内存
LFU(Least Frequently Used):
- 核心思想:淘汰访问频率最低的 key
- Redis 实现:记录 16bit 访问计数 + 8bit 衰减时间,采用近似 LFU
- 优势:更精准识别真正的"热 key",避免 LRU 的"一次性扫描"问题
LFU 的衰减机制:
访问计数随时间衰减,避免"老热 key"长期占据内存 衰减周期可配置:lfu-decay-time(默认 1 分钟)三、缓存三大问题:穿透、击穿、雪崩
在高并发场景下,Redis 缓存失效可能导致数据库被瞬间压垮。这三个问题虽然名字相似,但成因和解决方案完全不同:
3.1 缓存穿透(Cache Penetration)
问题描述:查询一个不存在的数据,由于缓存中没有,请求直接打到数据库。如果攻击者大量构造不存在的 key 进行查询,数据库将承受巨大压力。
典型场景:
- 恶意攻击:构造大量不存在的商品 ID 查询
- 业务逻辑缺陷:查询已被删除的数据
解决方案:
方案一:缓存空值(最简单)
// 查询数据库未命中,缓存空值(设置较短 TTL)Stringdata=redisTemplate.opsForValue().get(key);if(data==null){data=db.query(key);if(data==null){// 缓存空值,防止重复查询 DBredisTemplate.opsForValue().set(key,"",Duration.ofMinutes(5));}else{redisTemplate.opsForValue().set(key,data,Duration.ofHours(1));}}缺点:缓存大量空值会占用内存,且存在短暂的数据不一致窗口。
方案二:布隆过滤器(推荐)
布隆过滤器是一种概率型数据结构,用于判断某个元素是否可能在集合中:
- 一定不存在:如果某个 bit 为 0,该 key 绝对不在集合中
- 可能存在:如果所有 bit 都为 1,该 key 可能在集合中(存在误判率)
Redis 实现:
# 使用 RedisBloom 模块BF.ADDusersuser:1001 BF.ADDusersuser:1002# 查询BF.EXISTSusersuser:1003# 返回 0 → 一定不存在,直接返回# 返回 1 → 可能存在,继续查缓存/DB布隆过滤器参数选择:
| 误判率 | 哈希函数数 | 每个元素占用空间 |
|---|---|---|
| 1% | 7 | 9.6 bits |
| 0.1% | 10 | 14.4 bits |
| 0.01% | 13 | 19.2 bits |
注意:布隆过滤器不支持删除,因为删除一个 key 会影响其他 key 的判断。如果需要删除,可使用"计数布隆过滤器"或定期重建。
3.2 缓存击穿(Cache Breakdown)
问题描述:某个热点 key恰好过期,此时大量并发请求同时查询该 key,由于缓存未命中,所有请求同时打到数据库。
典型场景:
- 秒杀商品详情页缓存过期
- 热点新闻缓存失效
解决方案:
方案一:互斥锁(Mutex Lock)
publicStringgetHotData(Stringkey){Stringdata=redisTemplate.opsForValue().get(key);if(data!=null)returndata;// 获取互斥锁StringlockKey="lock:"+key;booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10));if(locked){try{// 双重检查data=redisTemplate.opsForValue().get(key);if(data==null){data=db.query(key);redisTemplate.opsForValue().set(key,data,Duration.ofHours(1));}}finally{// Lua 脚本释放锁(保证原子性)Stringscript="if redis.call('get', KEYS[1]) == ARGV[1] then "+"return redis.call('del', KEYS[1]) else return 0 end";redisTemplate.execute(newDefaultRedisScript<>(script,Long.class),Collections.singletonList(lockKey),"1");}}else{// 未获取到锁,短暂休眠后重试Thread.sleep(100);returngetHotData(key);}returndata;}方案二:逻辑过期(Logical Expiry)
// 缓存 value 结构:{ data: "...", expireTime: 1699123456000 }publicStringgetDataWithLogicExpiry(Stringkey){Stringjson=redisTemplate.opsForValue().get(key);if(json==null)returnnull;CacheDatacacheData=JSON.parseObject(json,CacheData.class);// 逻辑未过期,直接返回if(cacheData.getExpireTime()>System.currentTimeMillis()){returncacheData.getData();}// 逻辑已过期,获取锁后开启独立线程重建StringlockKey="lock:"+key;booleanlocked=redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10));if(locked){// 开启独立线程重建缓存CACHE_REBUILD_EXECUTOR.submit(()->{try{StringnewData=db.query(key);CacheDatanewCache=newCacheData(newData,System.currentTimeMillis()+Duration.ofHours(1).toMillis());redisTemplate.opsForValue().set(key,JSON.toJSONString(newCache));}finally{redisTemplate.delete(lockKey);}});}// 返回旧数据(逻辑过期但物理未删除)returncacheData.getData();}两种方案对比:
| 维度 | 互斥锁 | 逻辑过期 |
|---|---|---|
| 一致性 | 强一致性 | 短暂不一致 |
| 可用性 | 可能有等待延迟 | 高可用,无等待 |
| 实现复杂度 | 简单 | 较复杂 |
| 适用场景 | 库存、余额等强一致性场景 | 商品详情、配置等允许短暂不一致场景 |
3.3 缓存雪崩(Cache Avalanche)
问题描述:大量 key 同时过期或 Redis 宕机,导致所有请求同时打到数据库,数据库瞬间被压垮。
典型场景:
- 批量设置缓存时使用了相同的 TTL
- Redis 集群宕机
- 缓存服务重启
解决方案:
方案一:随机偏移 TTL
// 基础 TTL 1 小时,加上 0~10 分钟的随机偏移intbaseTtl=3600;intrandomOffset=ThreadLocalRandom.current().nextInt(0,600);redisTemplate.opsForValue().set(key,data,Duration.ofSeconds(baseTtl+randomOffset));方案二:多级缓存
客户端请求 ↓ 本地缓存 (Caffeine) —— 第一层,QPS 最高 ↓ Redis 缓存 —— 第二层,分布式共享 ↓ 数据库 —— 最后一层方案三:熔断降级
// 使用 Sentinel 或 Hystrix 实现熔断@SentinelResource(value="getData",fallback="getDataFallback")publicStringgetData(Stringkey){returnredisTemplate.opsForValue().get(key);}publicStringgetDataFallback(Stringkey){// 数据库压力过大时,直接返回默认值return"{"status":"degraded","data":null}";}方案四:高可用架构
- Redis 主从复制 + 哨兵模式(Sentinel)
- Redis Cluster 集群模式
- 异地多活部署
四、综合对比:三大问题速查表
| 问题 | 成因 | 核心特征 | 最佳解决方案 |
|---|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 数据库查询量异常增加 | 布隆过滤器 + 缓存空值 |
| 缓存击穿 | 热点 key 过期 | 单个 key 引发高并发查 DB | 互斥锁 / 逻辑过期 |
| 缓存雪崩 | 大量 key 同时过期 | 数据库瞬间被压垮 | 随机 TTL + 多级缓存 + 熔断 |
五、生产环境配置最佳实践
5.1 redis.conf 核心配置
# 内存上限maxmemory 4gb# 淘汰策略(纯缓存推荐 allkeys-lru)maxmemory-policy allkeys-lru# LFU 衰减时间(分钟)lfu-decay-time1# 过期 key 采样数量(影响定期删除效率)maxmemory-samples55.2 监控指标
# 查看内存使用情况INFO memory# 查看 key 过期统计INFO stats# expired_keys: 累计过期 key 数量# evicted_keys: 累计淘汰 key 数量# 查看慢查询SLOWLOG GET105.3 关键监控告警
| 指标 | 阈值 | 告警级别 |
|---|---|---|
| used_memory / maxmemory | > 80% | 警告 |
| evicted_keys 增长速率 | > 1000/min | 严重 |
| expired_keys 堆积 | 大量 key 未过期 | 警告 |
| 缓存命中率 | < 90% | 警告 |
六、总结
本文系统梳理了 Redis 的三大核心运维议题:
- 过期删除策略:惰性删除保证实时性,定期删除防止内存泄漏,两者互补
- 内存淘汰策略:8 种策略中,
allkeys-lru和allkeys-lfu是生产环境首选 - 缓存三大问题:穿透用布隆过滤器,击穿用互斥锁/逻辑过期,雪崩用随机 TTL + 多级缓存
理解这些机制,不仅能帮助你在面试中从容应对,更能在生产环境中提前规避风险,保障系统稳定性。