前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >缓存穿透、缓存击穿、缓存雪崩看这篇就够了,文末还送福利哦!

缓存穿透、缓存击穿、缓存雪崩看这篇就够了,文末还送福利哦!

作者头像
码农神说
修改2020-08-09 09:01:05
6430
修改2020-08-09 09:01:05
举报
文章被收录于专栏:码农神说

当我们进行架构设计时,缓存是提高高性能的最重要也是最常用的组件之一。数据库的瓶颈在于磁盘I/O,虽然现如今关系数据库的部分应用场景采用了NoSQL作为替代,但依然没能摆脱磁盘I/O的性能问题。缓存的妙处就是在提高性能的同时,也保护了下游数据库,避免I/O压力过大导致宕机。

常用的缓存有单机版的EhCache、分布式版的Memcache和Redis,都属于K-V类型的存储,Key与Value一一对应。当然如果较真的话他们也属于NoSQL范畴,比如Redis弥补了关系数据不能存储结构数据的缺憾。虽然这些缓存中间件已经非常成熟和稳定,也得到了广泛的应用,但设计一个良好的缓存系统还是会遇到很多问题需要解决、方案也需要取舍。

缓存的应用场景

每一个技术或中间件都有自己的应用场景,在这些场景中可以发挥它最大的优势。如果不熟悉他们的应用场景,愣是生搬硬套,只会事倍功半,南辕北辙。缓存的应用场景有如下几种:

  • 高频访问的数据:限于磁盘I/O的瓶颈,对于高频访问的数据,需要缓存起来提高性能,降低下游数据库的压力冲击;
  • 复杂运算的结果:对于需要耗费CPU、经过复杂运算才能获得的结果,需要缓存来,做到“一劳长时间逸",如count(id)统计论坛在线人数;
  • 读多写少:每次读都需要select甚至join很多表,数据库压力大,由于写得少,容易做到数据的一致性,非常适合缓存的应用;
  • 一致性要求低:由于缓存的数据来源于数据库,在高并发时数据不一致性就比较凸显,不一致的问题可以解决但代价不菲;

本篇主要来讲讲架构设计中的缓存穿透、缓存击穿、缓存雪崩产生的原因和缓解措施,技术来不得半点含糊,要知其然知其所以然。

缓存穿透 Cache Penetration

缓存穿透是指数据库中没有符合条件的数据,缓存服务器中也就没有缓存数据,导致业务系统每次都绕过缓存服务器查询下游的数据库,缓存服务器完全失去了其应用的作用。如果黑客试图发起针对该key的大量访问攻击,数据库将不堪重负,最终可能导致崩溃宕机。从上图可以看出直接穿过缓存到达下游数据库,大致业务流程如下

可采取的缓解措施

1.存储空值/默认值

虽然数据库中没有符合条件的数据,可以考虑缓存空值或适合业务的默认值,来缓解这种情况。为了降低数据的不一致需要注意两点:1. 缓存的过期时间需要设置的比较短;2. 当数据库数据更新时也需要及时更新缓存中对应的数据。

2.布隆过滤器(Bloom Filter)

布隆过滤器是一种比较巧妙的概率性数据结构,它可以告诉你数据一定不存在可能存在,相比Map、Set、List等传统数据结构它占用内存少、结构更高效。比如有一个下面这样的数据结构,每个存储位存储的都是一个big,即0或1

当我们向缓存中插入key为name的缓存数据时,先使用N种不同的hash函数做N次hash得到N个哈希值,在上面的数据结构中找到对应哈希值的下标,并把存储数据设置为1。假如N=3,我们可以使用hash1、hash2、hash3分别计算出了哈希值为8,15和13,则将其对应下标的数据设置为1,如下图

此时你如果想判断一个缓存key是否存在,就采用同样的l流程:3次hash、根据哈希值寻找下标、取出存储的数据。如果存储的数据不全都是1,也就意味着缓存中不存在此key,但都是1也只表示可能存在。不过没关系,我们只需要否定的意图就能达到目标了(O ^ ~ ^ O)。

以上两种缓解措施在不同的应用场景可以做些适当的选择:如果访问量大可以使用第一种方案简单粗暴;如果访问量低但涉及的key比较多,则可采用第二种方案。

缓存击穿 Cache Breakdown

缓存击穿是指当某一key的缓存过期时大并发量的请求同时访问此key,瞬间击穿缓存服务器直接访问数据库,让数据库处于负载的情况。

缓存击穿一般发生在高并发的互联网应用场景,可采用的如下的缓解措施

1.锁更新

可以使用(分布式)锁,只让一个线程更新key,其他线程等待,直到缓存更新释放锁。比如简单粗暴的synchronized关键字

代码语言:javascript
复制
public synchronized String getCacheData() {
  String cacheData = "";
  //Read redis
  cacheData = getDataFromRedis();
  if (cacheData.isEmpty()) {
    //Read database
    cacheData = getDataFromDB();
    //Write redis
    setDataToCache(cacheData);
  }
  return cacheData;
}

但synchronized这个锁太宽泛,会造成大量的请求阻塞,性能极低,进一步优化缩小锁的范围

代码语言:javascript
复制
static Object lock = new Object();

public String getCacheData() {
  String cacheData = "";
  //Read redis
  cacheData = getDataFromRedis();
  if (cacheData.isEmpty()) {
    synchronized (lock) {
      //Read database
      cacheData = getDataFromDB();
      //Write redis
      setDataToCache(cacheData);
    }
  }
  return cacheData;
}

学无止境,还可以继续优化,使用互斥锁 (✧◡✧):得到锁的线程就读数据写缓存,没得到锁的线程可以不用阻塞,继续从缓存中读数据,如果没有读到数据就休息会再来试试。

代码语言:javascript
复制
public String getCacheData(){
  String result = "";
  //Read redis
  result = getDataFromRedis();
  if (result.isEmpty()) {
    if (reenLock.tryLock()) {
      try {
        //Read database
        result = getDataFromDB();
        //Write redis
        setDataToCache(result);
      }catch(Exception e){
        //...
      }finally {
        reenLock.unlock (); // release lock
      }
    } else {
      //Note: this can be combined with the 
      // following double caching mechanism:

      //If you can't grab the lock, 
      // query the secondary cache

      //Read redis
      result = getDataFromRedis();
      if (result.isEmpty()) {
        try {
          Thread.sleep(100);
        } catch (InterruptedException e) {
          //...
        }
        return getCacheData();
      }
    }
  }
  return result;
}

2.异步更新

还有个可行的方案就是把缓存设置为永久不过期,异步定时更新缓存。比如后台有个值守线程专门定时更新缓存,但一般还要定时频繁地去检测缓存,一旦发现被踢掉(比如被缓存的失效策略FIFO、LFU、LRU等)需要立刻更新缓存,但这个“定时”的度是比较难掌握的,实现简单但用户体验一般。

异步更新机制还比较适合缓存预热,缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统,避免在用户请求时才缓存数据,提高了性能。

缓存雪崩 Cache Avalanche

缓存雪崩是指当大量缓存同时过期或缓存服务宕机,所有请求的都直接访问数据库,造成数据库高负载,影响性能,甚至数据库宕机,它和缓存击穿的区别在于失效key的数量。

可以采取的缓解措施

1.集群

高可用方案的本质就是冗余,集群是其实现方式之一,使用集群可以避免服务单点故障,但集群也带来了复杂度,好在很多成熟的中间件都有稳妥的集群方案,比如Redis集群。

2.过期时间

为了避免大量的缓存在同一时间过期,可以把不同的key过期时间随机生成,但随机可能会对业务有影响,但可以根据业务特点进行设置,总之是让过期时间分散。也有是通过定时刷新过期时间,类似于refresh token机制。

3.服务降级或熔断

服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。

服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。

熔断和降级都可以间接保证了整个系统的稳定性和可用性。

End

版权归@码农神说所有,转载须经授权,翻版必究

转载可联系助手,微信号:codeceo-01

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码农神说 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档