0%

缓存问题详解

缓存问题详解

一、缓存穿透(查不存在的数据)

问题描述:请求查询数据库中根本不存在的数据(比如ID=-1的数据),导致每次请求都绕过缓存直接访问数据库。

解决方案

  1. 缓存空对象
    • 原理:即使数据库查询为空,也把这个”空结果”缓存起来(例如存为null
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // 伪代码示例
      Object data = cache.get(key);
      if(data == null) {
      data = db.query(key);
      if(data == null) { // 数据库也没有
      cache.set(key, "NULL", 300); // 缓存空值5分钟
      } else {
      cache.set(key, data, 3600); // 缓存真实数据1小时
      }
      }
      return data.equals("NULL") ? null : data;
    • 注意事项:要给空值设置较短的TTL(如5分钟),避免占用太多内存
  1. 布隆过滤器(Bloom Filter)
    • 数据结构原理
      • 使用一个很大的位数组(bit array)和多个哈希函数
      • 插入元素时:用多个哈希函数计算元素的哈希值,将对应位数组位置设为1
      • 查询元素时:用同样的哈希函数计算,如果所有对应位都是1,则”可能存在”;如果有任一位是0,则”肯定不存在”
    • 特点
      • 空间效率极高(相比HashSet)
      • 判断存在时不一定存在,如果判断不存在则一定不存在

二、缓存击穿(热点key突然失效)

问题描述:缓存击穿是指当一个热点key在缓存中过期时,大量并发请求同时涌入,导致所有请求都直接访问数据库,造成数据库瞬时压力过大的现象。这种情况通常发生在高并发场景下对热点数据的访问。

解决方案

  1. 互斥锁方案

互斥锁方案通过控制只有一个线程能够重建缓存来解决击穿问题。

核心概念解释

  • 互斥锁(Mutex Lock):一种同步机制,确保同一时间只有一个线程可以访问特定资源或执行特定代码段。在分布式系统中,需要使用分布式锁来实现跨进程的互斥。
  • 分布式锁:在分布式环境下协调多节点访问共享资源的机制,常用Redis的SETNX命令(SET if Not eXists)实现,确保只有一个客户端能成功获取锁。

具体实现如下:

  • 当发现缓存失效时,线程首先尝试获取分布式锁
  • 获取锁成功的线程负责查询数据库并更新缓存
  • 其他线程等待锁释放后直接从缓存获取数据
  • 为避免死锁,需要设置合理的锁超时时间
  1. 逻辑过期方案

将过期时间存储在value中来实现热点key物理永不过期,逻辑判断过期:

  • 缓存数据包含实际数据和逻辑过期时间
  • 读取时检查逻辑过期时间
  • 如果数据已逻辑过期,触发异步更新
  • 始终返回数据,可能是脏数据

数据结构示例:

1
2
3
4
{
"data": "实际数据内容",
"expire": 114514
}

方案对比

方案 优点 缺点 适用场景
互斥锁 强一致性,不会返回旧数据 等待锁的线程会有延迟,实现较复杂 对一致性要求高的场景
逻辑过期 无等待时间,性能更好 可能返回脏数据,实现复杂 高并发且允许短暂不一致的场景

在实际应用中,可以根据业务需求选择合适的方案,或者组合使用这两种方案。对于核心业务数据可以采用互斥锁方案保证强一致性,对于非核心数据可以采用逻辑过期方案提高性能。

三、缓存雪崩(大量key同时失效)

问题描述:大量缓存key在同一时间过期,或Redis服务宕机,导致所有请求涌向数据库。

解决方案

  1. 差异化过期时间

    1
    2
    3
    // 基础过期时间 + 随机偏移量(0-300秒)
    int expireTime = 3600 + (int)(Math.random() * 300);
    cache.set(key, value, expireTime);
  2. 多级缓存架构

    1
    用户请求 → CDN缓存 → 前端本地缓存 → 应用级缓存(如Caffeine) → 分布式缓存(Redis) → 数据库