一、缓存穿透问题

缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,而且所有的请求都会经过数据库。

常见的解决方案有

  • 缓存空对象:
    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤:
    • 优点:内存占用较少,没有多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能
  1. 缓存空对象

    • 客户端访问不存在的数据,先请求Redis,Redis中没有此数据,接着访问数据库,数据库也没有此数据。这个数据穿透了缓存,直击数据库。(由于数据库能够承载的并发不如Redis,若大量请求访问数据库,则可能导致数据库瘫痪)
    • 解决方案:
      • 当请求数据库不存在的数据时,就把不存在的数据缓存到Redis中,并设置一个短期TTL
  2. 布隆过滤器

    • 其采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,用哈希思想去判断当前要查询数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问Redis,尽管此时Redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到Redis中。假设布隆过滤器判断这个数据不存在,则直接返回。
    • 优点:节约内存空间
    • 缺点:存在误判
      • 误判原因在于布隆过滤器采用了哈希思想,而基于哈希思想的机制,哈希冲突是其固有风险。

二、缓存雪崩问题

缓存雪崩是指在同一时段,大量的缓存Key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值。
  • 利用Redis集群提高服务的可用性。
  • 给缓存业务添加降级限流策略。
  • 给业务添加多级缓存。

缓存雪崩-zefuskdd.png

三、缓存击穿问题

缓存击穿问题(热点Key问题) 当一个被高并发访问且缓存重建业务交复杂的Key突然失效,大量的请求访问会在瞬间冲击数据库。

常见的解决方案:

  • 互斥锁
  • 逻辑过期

逻辑分析:

  • 线程1在缓存未命中后开始查询数据库并准备将数据回填至缓存。在此过程中,若线程1尚未完成,线程2、3、4等相继访问同一方法。由于数据尚未被线程1加载进缓存,这些线程均无法从缓存获取,导致它们同步转向数据库查询。这种并发场景下,多个线程同时访问数据库,造成数据库访问压力显著增大。
  1. 互斥锁: 为防止多线程并发访问导致数据库压力过大,可引入锁实现互斥访问,确保同一时刻仅一个线程访问数据库。但这会将原本并行的查询变为串行,影响性能。我们采用tryLock结合双检查(double check)策略:

    • 线程1查询缓存未命中,成功获取锁,独占执行数据库查询+缓存回填的逻辑。
    • 后续线程(如线程2)在尝试获取锁失败时,进入等待状态。
    • 线程1完成操作后释放锁,唤醒等待的线程2。
    • 线程2再次尝试获取锁(double check),发现缓存命中,从已填充数据的缓存中直接取数据,无需再访问数据库。
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void unlock(String key) {
    stringRedisTemplate.delete(key);
}

  1. 逻辑过期: 出现缓存击穿问题主要归因于对key设定了过期时间。为避免此问题又不长期占用内存,我们采用逻辑过期方案

    存储逻辑过期时间: 在Redis value中存放数据的过期时间,但不直接让Redis据此自动过期,而是由应用逻辑处理过期判断。

    • 线程1查询缓存,发现value中记录的数据已过期。
    • 线程1获取互斥锁,阻止其他线程同时访问。
    • 线程1启动新线程负责重构过期数据,自身则立即返回旧数据。

    并发访问处理:

    • 线程3在数据重建期间访问时,因锁被线程2持有,故无法获取,直接返回现有(可能已过期的)缓存数据。
    • 待新线程完成数据重构并更新缓存后,释放锁。
    • 此后访问的线程得以获取更新后的正确数据。

    优点:异步重建缓存,不影响主线程响应速度。
    缺点:在数据重建完成前,部分请求可能返回过期(脏)数据。

解决方案 优点 缺点
互斥锁 - 没有额外的内存消耗
- 保证一致性
- 实现简单
- 线程需要等待,性能受影响
- 可能有死锁风险
逻辑过期 - 线程无需等待,性能较好 - 不保证一致性
- 有额外内存消耗
- 实现复杂

图片和知识来源(结合自己的理解进行简化了许多):

努力有时候战胜不了天分,但至少能让别人看得起你