Redis 缓存
Redis 的过期缓存策略?如果内存满了怎么办
Redis 删除过期 key 主要靠两种策略:
- 惰性删除:访问 key 时检查是否过期,过期就删除。
- 定期删除:后台周期性抽样检查一批 key,删除其中过期的 key。
只用惰性删除会导致冷 key 过期后长期占内存;只用定期删除又会消耗太多 CPU。所以 Redis 两者结合。
如果内存达到 maxmemory,Redis 会按配置的内存淘汰策略处理。没有配置淘汰或策略不允许淘汰时,写命令可能报错。
Redis 的内存淘汰策略有哪几种
常见策略可以分三类:
- 不淘汰:
noeviction,内存满后写入报错。 - 只淘汰设置了过期时间的 key:
volatile-lru、volatile-lfu、volatile-random、volatile-ttl。 - 从所有 key 中淘汰:
allkeys-lru、allkeys-lfu、allkeys-random。
LRU 看最近是否使用,LFU 看访问频率。缓存场景一般常用 allkeys-lru 或 allkeys-lfu;如果 Redis 里混放了缓存和不能丢的数据,就要谨慎配置,最好从架构上拆实例。
什么是大 key 和热 key,分别有什么危害?怎么处理
大 key 指 value 很大,或集合元素特别多的 key,比如一个 Hash 有几十万个字段,一个 String 有几十 MB。危害是网络传输慢、删除阻塞、迁移困难、内存不均、主从复制压力大。
处理方式:
- 拆分 key,例如按业务 ID 分片。
- 避免一次性读取全量集合,用
scan、hscan、分页范围查询。 - 删除时用
unlink异步删除,少用del删除巨大对象。 - 用
memory usage、redis-cli --bigkeys、监控采样发现大 key。
热 key 指访问特别集中的 key,比如秒杀商品库存、首页配置。危害是单个 Redis 节点或分片被打满,甚至拖垮整个服务。
处理方式:
- 本地缓存热点数据。
- 热 key 拆分成多个副本 key,读请求随机打散。
- 加限流、降级、预热。
- 使用读写分离或 Cluster 分散压力,但单个 key 在 Cluster 中仍只落一个 slot,不能天然分散。
什么是延迟双删
延迟双删是缓存和数据库一致性的一种补偿手段,流程通常是:
- 先删除缓存。
- 更新数据库。
- 等待一小段时间。
- 再删除一次缓存。
它解决的是“先删缓存后改库”期间,有并发读把旧数据重新写入缓存的问题。第二次删除可以把这份旧缓存清掉。
缺点是延迟时间不好确定,太短可能删不到旧缓存,太长不一致窗口变大。所以它只能降低不一致概率,不能保证强一致。更常见的工程方案是更新数据库后删除缓存,并配合 MQ 重试、binlog 监听和 TTL 兜底。
什么是缓存穿透
缓存穿透是请求的数据在缓存和数据库里都不存在,例如恶意请求不存在的用户 ID。每次缓存未命中都会打到数据库,缓存没有起到保护作用。
解决方式:
- 参数校验:非法 ID 直接拦截。
- 缓存空值:数据库查不到时缓存一个空结果,并设置较短 TTL。
- 布隆过滤器:把可能存在的 ID 放进过滤器,不存在的请求提前拦截。
布隆过滤器可能误判存在,但不会误判不存在,适合挡掉大量无效请求。
什么是缓存击穿
缓存击穿是某个热点 key 过期的一瞬间,大量请求同时打到数据库。例如秒杀商品详情缓存失效,所有请求都去查 MySQL。
解决方式:
- 热点 key 不设置过短 TTL,或逻辑过期。
- 缓存重建时加互斥锁,只有一个线程查数据库并回填缓存。
- 提前预热热点数据。
- 对异常流量限流降级。
什么是缓存雪崩
缓存雪崩是大量 key 在同一时间失效,或者 Redis 整体不可用,导致请求集中打到数据库。
解决方式:
- TTL 加随机值,避免同一时间集中失效。
- 热点数据预热。
- 多级缓存,本地缓存兜底。
- Redis 高可用,主从、哨兵或 Cluster。
- 服务限流、熔断、降级,保护数据库。
缓存穿透、击穿、雪崩有什么区别,如何解决
| 问题 | 核心原因 | 典型表现 | 解决方案 |
|---|---|---|---|
| 穿透 | 数据本身不存在 | 每次都查库 | 参数校验、空值缓存、布隆过滤器 |
| 击穿 | 单个热点 key 失效 | 热点请求集中查库 | 互斥锁、逻辑过期、预热 |
| 雪崩 | 大量 key 失效或 Redis 故障 | 大面积请求查库 | TTL 随机、高可用、限流降级 |
简单记:穿透是“查不存在”,击穿是“一个热点失效”,雪崩是“一大片失效或 Redis 挂了”。
如何保持缓存与数据库的双写一致性
常用方案是 Cache-Aside:读时先读缓存,缓存没有再查数据库并回填;写时先更新数据库,成功后删除缓存。
为什么不推荐“先删缓存,再更新数据库”?因为并发下可能把旧数据写回缓存:
- A 删除缓存。
- B 读缓存未命中,查数据库拿到旧值。
- B 把旧值写入缓存。
- A 更新数据库成功。
这时缓存里仍然是旧值。
为什么不推荐“更新数据库后直接更新缓存”?因为并发写时容易出现后完成的旧请求覆盖新缓存,而且缓存值可能不是数据库字段的简单映射,维护成本高。
所以更常见的做法是:先更新数据库,再删除缓存。如果删除失败,配合 MQ 重试、binlog 监听或定时补偿;同时给缓存设置 TTL 作为最终兜底。
这套方案保证的是最终一致,不是强一致。如果业务必须强一致,就不应该依赖缓存读结果,应该读数据库或用锁/事务约束关键流程。
先更新数据库再删缓存,缓存删除失败了怎么办
缓存删除失败会导致旧缓存继续存在。常见原因包括 Redis 超时、网络抖动、Redis 主从切换、程序在事务提交后还没删缓存就异常退出。
解决方案:
- 异步重试:删除失败后把 key 写入消息队列或重试表,后台持续重试。
- 设置 TTL:即使删除失败,缓存也会最终过期,这是兜底方案。
- binlog 监听:用 Canal 等组件监听 MySQL binlog,感知数据变更后异步删除缓存。
- 幂等删除:删除缓存本身天然可以重复执行,重试、补偿、binlog 消费都要按幂等设计。
注意数据库事务提交成功后再删缓存。如果事务还没提交就删缓存,并发读可能读到旧数据后又回填缓存。
二级缓存:本地 Caffeine + Redis + MySQL 如何保证数据一致性
二级缓存读流程:
- 先读本地 Caffeine,命中直接返回。
- 本地未命中读 Redis,命中后回填本地缓存。
- Redis 未命中读 MySQL,再回填 Redis 和本地缓存。
写流程一般还是先更新 MySQL,再删除 Redis,然后广播本地缓存失效消息,让所有应用节点删除自己的 Caffeine 缓存。
广播可以用 Redis Pub/Sub,也可以用 Kafka、RocketMQ 等消息队列。Pub/Sub 简单但消息不持久,节点重启或网络抖动可能错过消息;MQ 更适合严肃的一致性场景,因为可以持久化、重试和追踪消费状态。
本地缓存一定要设置较短 TTL 兜底,否则失效消息丢了之后,单个节点可能长期读到旧数据。