缓存穿透/击穿/雪崩及其解决方案

1. 缓存的穿透/击穿/雪崩

  • 缓存穿透:缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

  • 缓存击穿:缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。

  • 缓存雪崩:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

与缓存击穿不同的是:存击穿是热点key失效,缓存雪崩是大量的key同时失效。

2. 缓存处理流程

前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。

3. 缓存穿透

3.1. 描述

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

3.2. 解决方案

  1. 接口层增加校验
    如用户鉴权校验,id做基础校验,不符合规则的id的直接拦截;
  2. 缓存空数据
    从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null缓存,有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击;
  3. 布隆过滤器
    将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力;

4. 缓存击穿

4.1. 描述

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力;

4.2. 解决方案

  1. 设置热点数据永远不过期

    • 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
    • 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
    • 虽然简单,但也存在问题: 1.不保证一致性;2.代码复杂度增大(每个value都要维护一个timekey);
  2. 加互斥锁

    业界比较常用的做法,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
    互斥锁参考代码如下:

    public String get(key) {
        String value = redis.get(key);
        if (value == null) { //代表缓存值过期
            //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
            if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
                    value = db.get(key);
                    redis.set(key, value, expire_secs);
                    redis.del(key_mutex);
                } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                    sleep(50);
                    get(key);  //重试
                }
            } else {
                return value;
            }
        }
    }

5. 缓存雪崩

5.1. 描述

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

5.2. 解决方案

  1. 过期时间设置随机
    缓存数据过期时间随机,防止同一时间大量数据过期现象发生。
  2. 加锁或者队列的方式
    加锁或队列方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。
  3. 分布在不同高度缓存
    如果缓存数据库是分布式部署,将热点数据均匀分布在不同高的缓存数据库中。

发表评论

邮箱地址不会被公开。 必填项已用*标注