参考文献

七大缓存经典问题

缓存穿透(一穿到底)

原因分析
  • 缓存穿透存在的原因:用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

    • 如用户访问的是一个不存在的 key,查 DB 返回空(即一个 NULL),那就不会把这个空写回 cache.那以后不管查询多少次这个不存在的 key,都会Cache Miss,都会查询 DB.整个系统就会退化成一个 “前端 + DB“的系统,由于 DB 的吞吐只在Cache 的 1%~2% 以下,如果有特殊访客,大量访问这些不存在的 key,就会导致系统的性能严重退化,影响正常用户的访问.
  • 缓存穿透会发⽣在什么时候呢?⼀般来说,有两种情况

    • 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
    • 恶意攻击:专⻔访问数据库中没有的数据。
处理办法
  • 验证拦截

    • 接口层进行校验拦截,对于一些可预知的非法参数进行拦截,如查询ID字段,传入的值为负值的情况;
  • 回种空值

    • 查询这些不存在的数据时,第一次查 DB,虽然没查到结果返回 NULL,仍然记录这个 key 到缓存,只是这个 key 对应的 value 是一个特殊设置的值.但是如果特殊访客持续访问大量的不存在的 key,这些 key 即便只存一个简单的默认值,也会占用大量的缓存空间,导致正常 key 的命中率下降.所以进一步的改进措施是,对这些不存在的 key 只存较短的时间,让它们尽快过期;或者将这些不存在的 key 存在一个独立的公共缓存,从缓存查找时,先查正常的缓存组件,如果 miss,则查一下公共的非法 key 的缓存,如果后者命中,直接返回,否则穿透 DB,如果查出来是空,则回种到非法 key 缓存,否则回种到正常缓存.
  • 使用布隆过滤器

    • 构建一个 BloomFilter 缓存过滤器,记录全量数据,这样访问数据时,可以直接通过 BloomFilter 判断这个 key 是否存在,如果不存在直接返回即可,根本无需查缓存和 DB.但是 BloomFilter 要缓存全量的 key,这就要求全量的 key 数量不大,10 亿条数据以内最佳,因为 10 亿 条数据大概要占用 1.2GB 的内存.也可以用 BloomFilter 缓存非法 key,每次发现一个 key 是不存在的非法 key,就记录到 BloomFilter 中,这种记录方案,会导致BloomFilter存储的 key 持续高速增长,为了避免记录 key 太多而导致误判率增大,需要定期清零处理.
    • 布隆过滤器缺点:
      • 它在判断元素是否在集合中时是有一定错误几率的,比如它会把不是集合中的元素判断为处在集合中.
        • 选择多个 Hash 函数计算多个 Hash 值,这样可以减少误判的几率.
      • 不支持删除元素.

缓存雪崩(大量失效)

原因分析
  • 缓存雪崩是指⼤量的应⽤请求⽆法在Redis缓存中进⾏处理,紧接着,应⽤将⼤量请求发送到数据库层,导
    致数据库层的压⼒激增.
    • 第⼀个原因是:缓存中有⼤量数据同时过期,导致⼤量请求⽆法得到处理.

      • 具体来说,当数据保存在缓存中,并且设置了过期时间时,如果在某⼀个时刻,⼤量数据同时过期,此时,应⽤再访问这些数据的话,就会发⽣缓存缺失。紧接着,应⽤就会把请求发送给数据库,从数据库中读取数据。如果应⽤的并发请求量很⼤,那么数据库的压⼒也就很⼤,这会进⼀步影响到数据库的其他正常业务请求处理。
    • 第二个原因是: Redis缓存实例发生故障宕机,无法处理请求.

"⼤量数据同时过期"处理方法
  • 加互斥锁:跟缓存击穿解决思路一致,同一时间只让一个线程构建缓存,其他线程阻塞排队.

  • 缓存永不过期:设置key永不失效(热点数据);

  • 均匀过期:设置key缓存失效时候尽可能错开(把每个 Key 的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效);

  • 双层缓存策略:比如同时使用redis和memcache缓存,请求->redis->memcache->db;

    使用主备两层缓存:

    • 主缓存:有效期按照经验值设置,设置为主读取的缓存,主缓存失效后从数据库加载最新值.

    • 备份缓存:有效期长,获取锁失败时读取的缓存,主缓存更新时需要同步更新备份缓存.

  • 服务降级,是指发⽣缓存雪崩时,针对不同的数据采取不同的处理⽅式。

    • 当业务应⽤访问的是⾮核⼼数据(例如电商商品属性)时,暂时停⽌从缓存中查询这些数据,⽽是直接返 回预定义信息、空值或是错误信息;
    • 当业务应⽤访问的是核⼼数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续 通过数据库读取。
"Redis实例宕机"处理方法
  • 在业务系统中实现服务熔断或请求限流
    • 所谓的服务熔断,是指在发⽣缓存雪崩时,为了防⽌引发连锁的数据库雪崩,甚⾄是整个系统的崩溃,我们 暂停业务应⽤对缓存系统的接⼝访问。再具体点说,就是业务应⽤调⽤缓存接⼝时,缓存客⼾端并不把请求 发给Redis缓存实例,⽽是直接返回,等到Redis缓存实例重新恢复服务后,再允许应⽤请求发送到缓存系统。
    • 这样⼀来,我们就避免了⼤量请求因缓存缺失,⽽积压到数据库系统,保证了数据库系统的正常运⾏。

缓存数据一致性

原因分析
  • 缓存中的数据和数据库中的数据不一致,会导致系统异常
处理办法
  • 使用缓存更新策略
  • 使用数据同步机制
  • 使用读写分离

数据并发竞争

原因分析
  • 指多个线程同时对缓存中的数据进行读写操作,会导致缓存数据的不一致性
处理办法
  • 使用分布式锁
  • 使用版本号控制缓存数据的一致性

缓存击穿Hot Key

原因分析
  • 缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大.
处理办法
  • 使用互斥锁(mutex key)

  • 在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接击中缓存,防止了缓存击穿.

  • 热点数据永不过期

    • 永不过期实际包含两层意思:

      • 物理不过期,针对热点key不设置过期时间

      • 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建

缓存预热

原因分析
  • 指在系统启动或高峰期前,预先将热点数据加载到缓存中,以避免请求落到数据库上
处理办法
  • 使用缓存预热脚本
  • 在系统启动时预加载热点数据
  • 使用定时预加载数据

缓存降级

原因分析
  • 指高峰期或系统故障时,为了保证系统的可用性,临时关闭缓存服务或将缓存服务降级,以避免由于缓存导致系统崩溃
处理办法
  • 使用限流、熔断或降级等手段来保证系统的可用性

总结

问题 原因 应对方案
缓存雪崩 大量数据同时过期
缓存实例宕机
给缓存数据的过期时间加上小的随机数据,避免同时过期
服务降级
服务熔断
请求限流
Redis缓存主从集群
缓存击穿 访问非常频繁的热点数据过期 不给热点数据设置过期时间,一直保留
缓存穿透 缓存和数据库中都没有要访问的数据 缓存空值或缺省值
请使用布隆过滤器快速判断
请求入口前端对请求合法性进行检查

缓存的读写模式

img

  • 上图来源于https://blog.bytebytego.com/p/top-caching-strategies

Cache Aside(旁路缓存)

  • 更新数据时不更新缓存,而是删除缓存中的数据,在读取数据是,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中.
  • 这种策略数据以数据库中的数据为准,缓存中的数据是按需加载的.
  • 可以分为读策略写策略
    • 读策略步骤:
      • 从缓存中读取数据,若缓存中命中,则直接返回数据;
      • 若缓存不命中,则从数据库中查询数据;
      • 查询到数据后,将数据写入到缓存中,并且返回给用户.
    • 写策略步骤: 先操作数据库,再删除缓存
      • 更新数据库中的数据记录
      • 删除缓存记录.
  • Cache Aside 存在的最大的问题是当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响.如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:
    1. 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了.当然这么做对于写入的性能会有一些影响;
    2. 另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快地过期,对业务的影响也是可以接受.
  • 适用场景: 读多写少

Read/Write Through(读写穿透)

  • 由系统本身完成,数据库与缓存的问题交由系统本身去处理

Write Behind Caching(异步缓存写入)

  • 调用者只操作缓存,其他线程去异步处理数据库,实现最终一致