news 2026/6/14 5:24:13

Redis篇(三):持久化(上)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis篇(三):持久化(上)

一、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 没有选择它,原因有二:

  1. CPU 开销大:需要维护大量定时器,消耗大量 CPU 资源
  2. 实现复杂:需要精确的时间管理,增加系统复杂度

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-ttlTTL 优先优先淘汰即将过期的 key
volatile-lruLRU 淘汰从设置了 TTL 的 key 中淘汰最久未使用的
allkeys-lruLRU 淘汰从所有 key 中淘汰最久未使用的
volatile-lfuLFU 淘汰从设置了 TTL 的 key 中淘汰使用频率最低的
allkeys-lfuLFU 淘汰从所有 key 中淘汰使用频率最低的

2.2 配置方法

# redis.confmaxmemory 256mb maxmemory-policy allkeys-lru# 动态修改(无需重启)CONFIG SET maxmemory-policy allkeys-lfu

2.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%79.6 bits
0.1%1014.4 bits
0.01%1319.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-samples5

5.2 监控指标

# 查看内存使用情况INFO memory# 查看 key 过期统计INFO stats# expired_keys: 累计过期 key 数量# evicted_keys: 累计淘汰 key 数量# 查看慢查询SLOWLOG GET10

5.3 关键监控告警

指标阈值告警级别
used_memory / maxmemory> 80%警告
evicted_keys 增长速率> 1000/min严重
expired_keys 堆积大量 key 未过期警告
缓存命中率< 90%警告

六、总结

本文系统梳理了 Redis 的三大核心运维议题:

  1. 过期删除策略:惰性删除保证实时性,定期删除防止内存泄漏,两者互补
  2. 内存淘汰策略:8 种策略中,allkeys-lruallkeys-lfu是生产环境首选
  3. 缓存三大问题:穿透用布隆过滤器,击穿用互斥锁/逻辑过期,雪崩用随机 TTL + 多级缓存

理解这些机制,不仅能帮助你在面试中从容应对,更能在生产环境中提前规避风险,保障系统稳定性。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/14 5:22:04

告别命令行恐惧:用VS Code在Mac上可视化搞定Java环境与Maven依赖

告别命令行恐惧&#xff1a;用VS Code在Mac上可视化搞定Java环境与Maven依赖对于许多刚接触Java开发的开发者来说&#xff0c;命令行操作往往是一道难以逾越的门槛。特别是在Mac系统上配置Java开发环境时&#xff0c;需要频繁使用终端命令来设置环境变量、安装依赖&#xff0c;…

作者头像 李华
网站建设 2026/6/14 5:20:58

别再纠结了!5款热门PD SINK芯片(ECP5701/FS312A/CH221K等)选型实战指南

5款热门PD SINK芯片实战选型&#xff1a;从参数对比到真实项目决策 最近在开发一款支持20V输入的桌面无线充电器时&#xff0c;我花了整整两周时间对比市面上主流的PD SINK芯片。作为硬件开发者&#xff0c;选型过程往往充满纠结——每款芯片的规格书都宣称自己是最佳选择&…

作者头像 李华
网站建设 2026/6/14 5:15:07

【Springboot毕设全套源码+文档】基于springboot+vue的养老院系统的设计与实现(丰富项目+远程调试+讲解+定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/6/14 5:09:37

避开Keil的坑:STM32F407 CCM内存配置的正确姿势(附.sct文件模板)

STM32F407 CCM内存配置实战&#xff1a;从Keil陷阱到.sct文件精修第一次在STM32F407项目中使用CCM内存时&#xff0c;我像大多数开发者一样&#xff0c;自信满满地勾选了Keil的Memory配置选项&#xff0c;结果程序运行时出现的各种诡异崩溃让我百思不得其解。直到深夜查看.map文…

作者头像 李华