当前位置: 首页 > news >正文

Reids 如何处理缓存穿透、缓存击穿、缓存雪崩问题?

Redis 本身是一个高性能的键值存储系统,它提供了一些基础功能,但解决缓存穿透、缓存击穿和缓存雪崩等问题,更多的是依赖于应用层面的策略,并结合 Redis 提供的特性来实现。

下面分别说明这三个问题以及基于 Redis 的常见解决方案:

1. 缓存穿透 (Cache Penetration)

问题描述:
攻击者或用户恶意请求一个在缓存和数据库中都不存在的数据。这样,每次请求都会穿透缓存,直接打到数据库上,导致数据库压力过大,甚至宕机。

解决方案:

  1. 缓存空对象 (Cache Null Values / Cache Empty Objects):

    • 做法: 当数据库查询返回空(即数据不存在)时,仍然将这个“空结果”或一个特殊约定的占位符(如 null_value)缓存起来,并设置一个较短的过时时间(例如几分钟)。
    • 优点: 简单有效,后续对同一不存在Key的请求会直接从缓存获取空对象,避免了对数据库的重复查询。
    • Redis 实现: SET non_existent_key "null_value" EX <short_ttl>
    • 缺点:
      • 需要消耗额外的缓存空间存储空对象。
      • 如果攻击者构造大量不同的不存在的key,依然可能消耗大量缓存。
      • 数据一致性问题:如果在缓存空对象的短时间内,数据库中恰好插入了这条数据,那么缓存中的空对象会导致应用在短期内无法获取到新数据。
  2. 布隆过滤器 (Bloom Filter):

    • 做法: 将所有可能存在的数据的 Key 哈希到一个足够大的位数组(bitmap)中。当一个请求到来时,先通过布隆过滤器判断 Key 是否可能存在。
      • 如果布隆过滤器判断 Key 不存在,则直接返回,不查询缓存和数据库。
      • 如果布隆过滤器判断 Key 可能存在,则继续查询缓存和数据库。(布隆过滤器有误判率,即它可能将不存在的 Key 判断为存在,但不会将存在的 Key 判断为不存在)。
    • 优点: 空间效率和查询时间都非常好,能有效拦截大部分对不存在 Key 的请求。
    • Redis 实现: Redis 4.0 之后通过插件 RedisBloom 提供了布隆过滤器功能。也可以在应用层面自行实现或使用 Guava 的 BloomFilter,并将位数组存储在 Redis 中。
    • 缺点:
      • 实现相对复杂。
      • 存在一定的误判率(可以通过调整参数控制)。
      • 布隆过滤器不支持删除元素(标准布隆过滤器)。如果要支持删除,需要使用 Counting Bloom Filter 等变体,空间占用会增加。
      • 初始化和数据同步:需要将全量或热点数据的 Key 加入布隆过滤器。
  3. 接口层参数校验:

    • 做法: 对请求参数进行合法性校验,例如用户ID是否为正整数、长度是否符合规范等。不合法的请求直接拦截。
    • 优点: 简单,能拦截一些明显不合法的请求。
    • 缺点: 无法防止构造合法的、但不存在的Key的攻击。

2. 缓存击穿 (Cache Breakdown / Hotspot Invalid)

问题描述:
某一个热点 Key(高并发访问、数据不常变动)在缓存中过期失效的瞬间,大量并发请求同时涌向这个 Key。由于缓存未命中,这些请求会同时穿透缓存,直接打到数据库上,导致数据库压力瞬时增大,甚至崩溃。

解决方案:

  1. 互斥锁 (Mutex Lock / Distributed Lock):

    • 做法: 当缓存未命中时,不是所有请求都去查数据库。而是先尝试获取一个互斥锁(例如使用 Redis 的 SETNX 命令或 Redisson 提供的分布式锁)。
      • 获取到锁的线程/进程负责去数据库查询数据,并将数据写回缓存,然后释放锁。
      • 其他未获取到锁的线程/进程则等待一段时间后重试(双重检查锁模式),或者直接返回一个预设的默认值/稍旧的数据。
    • 优点: 只允许一个请求去重建缓存,有效防止对数据库的并发冲击。
    • Redis 实现:
      // 伪代码 - 双重检查锁 + Redis SETNX
      String value = redis.get(key);
      if (value == null) { // 缓存不存在if (redis.setnx(lockKey, "1", lock_ttl)) { // 尝试获取锁try {value = db.query(key); // 从数据库查询if (value != null) {redis.set(key, value, cache_ttl); // 写回缓存} else {redis.set(key, "null_value", short_ttl); // 防止穿透}} finally {redis.del(lockKey); // 释放锁}} else { // 未获取到锁Thread.sleep(50); // 等待一小会return redis.get(key); // 重试获取缓存}
      }
      return value;
      
    • 缺点:
      • 实现相对复杂,需要处理锁的获取、释放、超时等问题。
      • 如果获取锁的线程在重建缓存时失败,其他线程可能长时间等待。
  2. 热点数据永不过期 (Logical Expiration / Never Expire):

    • 做法: 对于极热点的数据,不设置物理上的 TTL (Time To Live),或者设置一个非常长的 TTL。而是在缓存值中额外存储一个逻辑过期时间字段。
      • 当应用访问缓存时,如果发现数据已逻辑过期,则启动一个后台异步线程去更新缓存中的数据,当前请求仍然返回旧的(但可接受的)缓存数据。
    • 优点: 避免了热点 Key 过期瞬间的并发问题,保证了高可用性。
    • 缺点:
      • 实现复杂,需要额外的逻辑来判断和更新。
      • 数据一致性会有一定延迟。
      • 需要一个机制(如定时任务或消息队列)来定期或触发式地更新逻辑过期的数据。
  3. 提前续期 (Proactive Renewal / Early Expiration):

    • 做法: 对于热点数据,在缓存过期之前,如果检测到有访问,就主动延长其过期时间。或者设置一个后台任务,在数据即将过期前主动刷新缓存。
    • 优点: 保证热点数据总是在缓存中。
    • 缺点: 需要额外的机制来监控和续期。

3. 缓存雪崩 (Cache Avalanche)

问题描述:
缓存雪崩有两种情况:

  1. 大量 Key 同时过期: 大量缓存 Key 在同一时间点集中过期失效(例如,应用重启后所有缓存都失效了,或者一批 Key 设置了相同的过期时间)。此时,大量的请求都会穿透缓存,直接打到数据库上,导致数据库压力剧增。
  2. Redis 实例宕机: Redis 缓存服务整体不可用(例如 Redis 节点故障、网络分区),导致所有请求都直接访问数据库。

解决方案:

针对大量 Key 同时过期:

  1. 过期时间打散 (Randomized Expiration Times):

    • 做法: 在设置缓存的过期时间 TTL 时,在基础过期时间上增加一个随机值(例如,基础 TTL 是 30 分钟,随机范围是 1-5 分钟,那么最终 TTL 就是 30 到 35 分钟之间的某个值)。
    • 优点: 使得 Key 的过期时间点分散开,避免了集中失效。简单有效。
    • Redis 实现: SET key value EX (base_ttl + random_offset)
  2. 使用互斥锁或逻辑过期:

    • 对于热点数据,可以采用缓存击穿中提到的互斥锁或逻辑过期方案,这也能在一定程度上缓解雪崩时单个 Key 的压力。

针对 Redis 实例宕机:

  1. Redis 高可用集群 (Redis Sentinel / Redis Cluster):

    • 做法: 搭建 Redis Sentinel(哨兵模式)实现主从复制和故障自动切换,或者使用 Redis Cluster 实现分布式和数据分片,并自带故障转移能力。
    • 优点: 保证 Redis 服务的整体可用性,即使部分节点宕机,服务也能继续。这是最根本的解决方案。
    • 缺点: 运维复杂度增加。
  2. 多级缓存 (Multi-Level Caching):

    • 做法: 例如,应用本地缓存 (如 Guava Cache, Caffeine) + 分布式缓存 (Redis)。当 Redis 宕机时,本地缓存仍然可以承担一部分请求。
    • 优点: 增加了一层防护。
    • 缺点: 增加了系统复杂度,需要考虑数据一致性问题。
  3. 服务降级与熔断 (Service Degradation and Circuit Breaking):

    • 做法:
      • 降级: 当检测到数据库压力过大或 Redis 不可用时,应用可以临时关闭某些非核心功能,或者返回预设的默认值、静态页面,以保证核心功能的可用性。
      • 熔断: 当依赖的服务(如数据库)持续出错达到一定阈值时,暂时切断对该服务的调用,一段时间后再尝试恢复。可以使用 Hystrix、Sentinel 等熔断组件。
    • 优点: 在极端情况下保护整个系统不被拖垮。
    • 缺点: 牺牲了部分用户体验或功能。
  4. 请求限流 (Rate Limiting):

    • 做法: 在应用入口层(如网关、Nginx)或应用内部,对单位时间内的请求数量进行限制。当请求超过阈值时,直接拒绝服务或引导用户排队。
    • 优点: 防止过量请求直接冲击后端。
    • 缺点: 可能会影响正常用户的使用。
  5. 数据预热 (Data Pre-warming):

    • 做法: 在系统启动或低峰期,提前将热点数据加载到缓存中。
    • 优点: 避免了系统刚启动时大量缓存未命中。

总的来说,Redis 提供了基础的缓存能力和一些原子操作(如 SETNX),而解决这些缓存问题通常需要结合应用层的设计模式和策略。选择哪种方案或方案组合,取决于具体的业务场景、系统架构和对一致性、可用性的要求。

相关文章:

  • 常用的Docker命令
  • 通用寄存器的 “不通用“ 陷阱:AX/CX/DX 的寻址禁区与突围之道
  • 代码训练LeetCode(22)研究者H指数
  • 防止网站被iframe嵌套的安全防护指南
  • 多线程编程技术解析及示例:pthread_cond_timedwait、pthread_mutex_lock 和 pthread_mutex_trylock
  • 数学知识体系难易程度表及关系
  • 蓝牙防丢器应用方案
  • 贝叶斯优化+LSTM+时序预测=Nature子刊!
  • Elasticsearch的写入性能优化
  • 高速ADC数据格式与JESD204B IP数据格式映射关系
  • FART 精准脱壳:通过配置文件控制脱壳节奏与范围
  • AI,如何重构理解、匹配与决策?
  • Oracle数据库笔记
  • [C]extern声明变量报错:undefined reference终极解决方案
  • 第五期书生大模型实战营-《L1G1-玩转书生大模型 API 之 Browser-Use 实践》
  • 若依Ruoyi中优先从本地文件加载静态资源
  • 理解网络协议
  • 3D动画在微信小程序的实现方法
  • el-amap-bezier-curve运用及线弧度设置
  • Vue前端篇——项目目录结构介绍