Redis缓存案例

wen java案例 1

五个生产级Redis缓存案例深度剖析

目录导读

  1. 缓存穿透的“布隆过滤器”防线 – 拦截不存在的数据请求
  2. 缓存击穿的“互斥锁”策略 – 热点Key失效时的并发保护
  3. 缓存雪崩的“过期时间错峰” – 分散失效时间避免集体宕机
  4. 双写一致性的“延迟双删”方案 – 解决数据库与缓存的数据漂移
  5. 缓存淘汰的“LRU与热点分离” – 让有限内存发挥最大价值
  6. 高频问答精选 – 面试与架构设计中的关键追问

缓存穿透:当查询“不存在”成为攻击入口

场景还原:某电商平台的商品详情接口,遭遇恶意脚本循环请求一个不存在的商品ID(如-1999999),由于Redis中无该Key,请求直接穿透至MySQL,导致数据库连接数飙升至极限,接口响应时间从20ms恶化至5s。

Redis缓存案例

传统方案缺陷:单纯设置空值缓存(如SET product:999999 null EX 60)虽然能暂时缓解,但攻击者可以换用无数个不存在的ID,空值缓存本身也会占用大量内存。

生产级解法:在Redis前增加布隆过滤器(Bloom Filter),初始化时,将所有存在的商品ID哈希映射到一个位数组(约500万商品,用10bit/个,仅需6MB内存),查询时:

if (bloomFilter.mightContain(productId)) {
    // 走Redis缓存查询
} else {
    return "商品不存在"; // 直接拒绝,不触达DB
}

实测效果:恶意请求命中率从100%下降至0.1%(布隆过滤器误判率),数据库QPS下降99.7%,注意布隆过滤器不支持删除,需每日定时重建(全量商品ID扫描)。


缓存击穿:热点Key的“单点熔断”危机

场景还原:某微博热搜榜的hot_ranking Key设置了2小时过期,当过期瞬间,突然涌入10万并发请求,全部未命中缓存而直击MySQL,导致数据库CPU瞬时100%。

方案对比

  • 分布式互斥锁(推荐):仅允许一个线程去查库并回填缓存,其余线程等待。
    # 伪代码
    value = redis.get(key)
    if value is None:
      if redis.setnx(key_mutex, "1", ex=10):  # 获得锁
          value = db.query(key)
          redis.set(key, value, ex=120)
          redis.delete(key_mutex)
      else:
          Thread.sleep(50)
          return redis.get(key)  # 自旋重试
  • 逻辑过期:Key不设置物理过期,而是value中存logicExpireTime,查询时若逻辑过期,则异步线程更新缓存,当前线程返回旧值,适合读多写少场景,但存在短暂数据不一致。

关键细节:锁的粒度必须细化到key_mutex = "lock:" + key,避免全局锁降低吞吐,重试次数建议上限3次,超出则返回降级数据(比如默认榜单)。


缓存雪崩:批量过期引发的“多米诺骨牌”

场景还原:促销活动首页的商品分类缓存(如category:1category:2等20个Key),统一在凌晨0点过期,0点后的10分钟内,大量冷门分类的请求压垮数据库。

分层防御策略

  1. 过期时间加随机扰动baseExpire + random(0, 300)秒,避免集体失效。
  2. 多级缓存兜底:本地(JVM/内存)缓存 + Redis缓存 + DB,即使Redis整体不可用,本地Caffeine缓存仍可提供5分钟旧数据。
  3. 熔断限流:使用Sentinel或Hystrix对DB访问降级,若Redis命中率低于30%,直接拒绝部分非核心请求。

最佳实践

  • 对于核心数据(如用户Session),使用永不过期+后台定时刷新。
  • 对于批量导入的配置数据(如城市列表),手动计算错峰过期时间,而非统一expire

双写一致性:更新数据库后缓存为何还是旧的?

场景还原:用户修改个人头像后,主页展示的仍是旧图片,原因在于:先更新DB,再删除缓存,但删除失败的瞬间,并发读请求又把旧数据写入了缓存。

业界常用方案——延迟双删

更新数据库
2. 删除缓存(第一次)
3. 休眠150ms(确保读请求已经结束,且把旧值重新写进缓存)
4. 再次删除缓存(第二次)

进阶修正:若第二次删除仍失败(如Redis故障),则借助订阅Binlog(如Canal)异步补偿删除,阿里内部更倾向使用“Cache Aside Pattern + 消息队列”:

  • 更新DB后发送MQ消息。
  • 消费者收到消息后,延迟1s执行删除操作。
  • 如果删除失败,重试3次,最后进入死信队列人工处理。

注意:强一致性需求(如库存扣减)不应依赖缓存,应直接读写DB,或使用Redis分布式锁+版本号校验。


缓存淘汰:内存不够时,谁该被优先牺牲?

场景还原:一个Redis实例分配了4GB内存,但业务塞入了8GB的热点新闻数据,导致OOM频繁宕机。

策略对比

  • allkeys-lru(默认):所有Key按最近最少使用淘汰,适合访问分布极度集中(二八原则)的场景。
  • volatile-ttl:仅淘汰设置了过期时间的Key中剩余TTL最小的,适合“临时数据”与“永久数据”混合的场景。
  • 热点Key内存分离:将单个超大Value(如一个5MB的JSON列表)拆分为多个小分片Key(如list:1list:2...),避免单个Key过期时引发大面积淘汰误伤。

生产优化

# 配置 maxmemory-policy 为 volatile-lfu (LFU比LRU更抗突发流量)
maxmemory-policy volatile-lfu

对Value进行压缩存储(如使用Snappy压缩JSON,可减少70%内存),对于死键,使用UNLINK异步删除(非阻塞)代替DEL(大Key阻塞线程)。


高频问答精选

Q1:缓存预热如何避免瞬间打满数据库? A:使用脚本提前将热门数据加载到Redis,加载时使用管道(Pipeline)批量写入,且控制并发线程数(如4线程),对未命中缓存的数据,在回源DB时加“错峰随机小延时”(如50-200ms),削峰填谷。

Q2:布隆过滤器误判率如何控制? A:公式误判率≈(1-e^(-k*n/m))^k,其中m为位数组大小,n为预期元素数,k为哈希函数数量,建议m = 10 * nk = 7时,误判率低于1%,生产上可用Google Guava的BloomFilter实现。

Q3:缓存与数据库最终一致性的最佳工程实践是什么? A:遵循“先更新DB,后删除缓存”为基础的Cache Aside模式,补充两点:1)删除失败必须重试(MQ或本地日志表);2)对于读多写极少的数据(如AB测试配置),设置短TTL(如5分钟)作为兜底。

Q4:为什么不用Redis分布式锁解决缓存击穿? A:锁本身增加了网络开销(可能比查DB更慢),若热点Key的DB查询只需20ms,而加锁+自旋平均等待100ms,则性能更差,只有DB查询耗时远大于锁等待时间的场景(gt;200ms的复杂聚合查询)才推荐加锁。

Q5:如何监控缓存命中率与延迟? A:使用Redis INFO stats中的keyspace_hitskeyspace_misses计算命中率,配合Prometheus+Grafana实时监控,设置告警线:核心链路命中率<95%触发警告,<85%触发紧急排查。


本文通过五个真实生产案例,剖析了Redis缓存从“能用”到“好用”的关键差异:穿透靠防御、击穿靠锁、雪崩靠错峰、一致性靠补偿、淘汰靠策略,每个方案都不是银弹,需结合业务访问特征选择组合,在架构评审时,不妨多问一句:“如果Redis节点挂了,我的降级方案是什么?”——这才是缓存设计的终极考题。

抱歉,评论功能暂时关闭!