缓存常见问题

因为引入缓存首先需求考虑的就是缓存更新的办法,之前在缓存更新的几种形式中我们介绍过。除了这个问题还有一些常见的问题,整理出一个表格,如下图所示:

缓存问题 发生原因 处理计划
缓存不共同 同步更新失利、异步更新 终究共同
缓存穿透 歹意进犯 空目标缓存、布隆过滤器
缓存击穿 热门key失效 互斥更新、随机退避、
缓存雪崩 缓存挂掉 快速失利熔断、主从形式、集群形式、差异失效时间
大key 存储value很大、调集数据过多、数据未整理 拆分key,整理key
热门key 预期外的拜访量陡增,如突然呈现的爆款产品 对key进行rehash然后仿制到不同集群,运用读写别离架构

数据不共同

共同性问题

数据不共同的问题,能够说只需运用缓存,就要考虑如何面对这个问题。缓存不共同发生的原因一般有两方面:

  • 选择缓存更新形式的不同形成的不共同,例如[缓存更新的几种形式]的Cache Aside不管是先更新db仍是先删去或更新cache,在高并发的状况下都有或许形成不共同的状况,只是不同的更新办法形成不共同的概率不相同,尽或许的选择形成不共同概率最小的更新形式。
  • 体系问题导致失利形成的不共同,在这里就是如缓存服务的机器宕机,网络反常形成的更新失利等。

处理计划

  • 采用强共同性协议,很少运用。
  • 终究共同性,在绝大部分场景中,特别是互联场景下,大多是确保终究共同性。
    • 重试机制mq
      1. 更新数据库,若这一步就失利,更新事务失利回滚
      2. 更新缓存失利,将失利的数据写入mq
      3. 消费mq得到失利的数据,从头删去缓存
    • 订阅数据库binlog[参阅MySQL仿制原理及运用canal],解耦缓存更新过程。

缓存穿透

问题

发生这个问题的原因或许是外部的歹意进犯,例如,对用户信息进行了缓存,但歹意进犯者运用不存在的用户id频频恳求接口,导致查询缓存不射中,然后穿透DB查询仍然不射中。这时会有很多恳求穿透缓存拜访到DB,添加数据库压力乃至导致体系宕机。

处理计划

  • 事务上做不合法参数的校验,尽量避免不合法恳求打到缓存。

  • 对不存在的用户,在缓存中保存一个空目标进行标记,避免相同ID再次拜访DB。不过有时这个办法并不能很好处理问题,或许导致缓存中存储很多无用数据。

  • 运用BloomFilter过滤器,BloomFilter的特点是存在性检测,假如BloomFilter中不存在,那么数据必定不存在;假如BloomFilter中存在,实践数据也有或许会不存在。非常适合处理这类的问题。

布隆过滤器

下面简略介绍下布隆过滤器,布隆过滤器内部维护一个bitArray(位数组), 开端所有数据全部置 0 。当一个元素过来时,能过多个哈希函数(hash1,hash2,hash3…)核算不同的在哈希值,并通过哈希值找到对应的bitArray下标处,将里面的值 0 置为 1 。需求阐明的是,布隆过滤器有一个误判率的概念,误判率越低,则数组越长,所占空间越大。误判率越高则数组越小,所占的空间越小。

缓存常见问题及解决方式

以上图为例,具体的写入过程(如有3个hash函数): 假设调集里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置为0。关于调集里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会发生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的方位标记为1。

查询a元素是否存在调会集的时候,相同的办法将a通过哈希映射到位数组上的3个点。假如3个点的其中有一个点不为1,则能够判别该元素必定不存在调会集。反之,假如3个点都为1,则该元素或许存在调会集。

留意:此处不能判别该元素是否必定存在调会集,或许存在必定的误判率。能够从图中能够看到:假设某个元素通过映射对应下标为4,5,6这3个点。尽管这3个点都为1,可是很明显这3个点是不同元素通过哈希得到的方位,因而这种状况阐明元素尽管不在调会集,也或许对应的都是1,这是误判率存在的原因。

布隆过滤器能确认一个值必定不存在,可是不能确认一个值必定存在。缓存穿透正好运用”布隆过滤器能确认一个值必定不存在”,因而不存在核算误差。

运用布隆过滤器处理缓存穿透

了解布隆过滤器原理后,我们用布隆过滤器处理缓存穿透问题就很简略了,在缓存前加一层布隆过滤器,运用布隆过滤器bitset存储结构存储数据库中所有值,查询缓存前,先查询布隆过滤器,若必定不存在就回来。

计划比照:

计划 运用场景 运用本钱
缓存空目标 1. 空数据量不大
2. 数据频频改变实时性高
1.代码维护简略
2.需求过多的缓存空间
3. 数据不共同
过滤器 1.数据量比较大
2. 数据射中不高
3. 数据相对固定实时性低
1.代码维护复杂
2.缓存空间占用少

缓存击穿

问题

缓存击穿,就是某个热门数据失效时,很多恳求这一时间都查不到缓存,然后全部恳求并发打到了数据库去查询数据构建缓存,形成数据库压力非常大乃至宕机。

处理计划

处理这个问题有如下办法:

  • 运用互斥锁更新,确保同一个进程中针对同一个数据不会并发恳求到DB,减小DB压力。
 public Object getCache(final String key) {
    Object value = redis.get(key);
    //缓存值过期
    if (value == null) {  
        //加mutexKey的互斥锁
        String mutexKey = mutexKey(key);
        if (redis.setnx(mutexKey, 1, time)) { 
            value = db.get(key);
            redis.set(key, value, time);
            redis.delete(mutexKey);
        } else {
            sleep(100);
            return get(key); 
        }
    }
    return value;
}
  • 不给热门数据设置过期时间,由后台异步更新缓存,或者在热门数据准备要过期前,提早告诉后台线程更新缓存以及从头设置过期时间;
办法 长处 缺点
互斥锁 1.简略易用2.共同性确保 1.存在线程堵塞的风险2.数据库拜访的压力转到分布式锁上来
异步更新 1.比较互斥锁计划,下降线程堵塞的时间 1.代码更复杂2.逻辑过期时间会占用必定的内存空间

缓存雪崩

问题

缓存雪崩。发生的原因是:

  • 很多恳求一起打到DB上,比方很多key一起过期
  • 缓存服务挂掉,这时所有的恳求都会穿透到DB。

处理计划

  • 运用快速失利的熔断限流策略,削减DB瞬间压力;
  • 运用主从形式和集群形式来尽量确保缓存服务的高可用。
  • 针对多个热门key一起失效的问题,能够在缓存时运用固定时间加上一个小的随机数,避免很多热门key同一时间失效。

大key及热门key

问题

大key及热门key的界说(不同公司根据实践状况界说不同):

名词 解释
大Key 一般以Key的巨细和Key中成员的数量来归纳断定,例如:
1. Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
2. Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
3. Key中成员的数据量过大:一个Hash类型的Key,它的成员数量尽管只有1,000个但这些成员的Value(值)总巨细为100 MB。
热Key 一般以其接收到的Key被恳求频率来断定,例如:
1. QPS会集在特定的Key:Redis实例的总QPS(每秒查询率)为10,000,而其中一个Key的每秒拜访量到达了7,000。
2. 带宽运用率会集在特定的Key:对一个具有上千个成员且总巨细为1 MB的HASH Key每秒发送很多的HGETALL操作恳求。
3. CPU运用时间占比会集在特定的Key:对一个具有数万个成员的Key(ZSET类型)每秒发送很多的ZRANGE操作恳求。

大key及热门key的问题

类别 阐明
大Key 1. 客户端履行命令的时长变慢。
2. Redis内存到达maxmemory参数界说的上限引发操作堵塞或重要的Key被逐出,乃至引发内存溢出(Out Of Memory)。
3. 集群架构下,某个数据分片的内存运用率远超其他数据分片,无法使数据分片的内存资源到达均衡。
4. 对大Key履行读恳求,会使Redis实例的带宽运用率被占满,导致本身服务变慢,一起易涉及相关的服务。
5. 对大Key履行删去操作,易形成主库较长时间的堵塞,进而或许引发同步中断或主从切换。
热门Key 1. 占用很多的CPU资源,影响其他恳求并导致整体性能下降。
2. 集群架构下,发生拜访倾斜,即某个数据分片被很多拜访,而其他数据分片处于闲暇状态,或许引起该数据分片的衔接数被耗尽,新的衔接树立恳求被拒绝等问题。
3. 在抢购或秒杀场景下,或许因产品对应库存Key的恳求量过大,超出Redis处理能力形成超卖。
4. 热Key的恳求压力数量超出Redis的承受能力易形成缓存击穿,即很多恳求将被直接指向后端的存储层,导致存储拜访量激增乃至宕机,然后影响其他事务。

处理计划

大key

  1. 单key存储value很大

    能够把value目标拆成多份,运用multiGet,这样做的意义在于削减操作在一个节点的压力,涣散到多个节点。

    运用hash,每个filed存储目标的各特点。

  2. 调集存储了过多的的值

    将这些元素分拆。以hash为例,原先的正常存取流程是

    hget(hashKey, field);
    hset(hashKey, field, value);
    

    现在,固定一个桶的数量,比方 1000, 每次存取的时候,先本地进行rehash,确认了该field落在哪个key上。

    newHashKey = hashKey + ( *hash*(field) % 1000; 
    hset (newHashKey, field, value) ; 
    hget(newHashKey, field);
    
  3. 定期删去过期大key

热门key

  1. 在Redis集群架构中对热Key进行仿制

    在Redis集群架构中,因为热Key的搬迁粒度问题,无法将恳求涣散至其他数据分片,导致单个数据分片的压力无法下降。此时,能够将对应热Key进行rehash后仿制并搬迁至其他数据分片,例如将热Key foo仿制出3个内容完全相同的Key并名为foo2、foo3、foo4,将这三个Key搬迁到其他数据分片来处理单个数据分片的热Key压力。

    该计划的缺点在于需求联动修改代码,一起带来了数据共同性的应战(由本来更新一个Key演变为需求更新多个Key),仅建议该计划用来处理暂时棘手的问题。

  2. 运用读写别离架构

    假如热Key的发生来自于读恳求,您能够将实例改形成读写别离架构来下降每个数据分片的读恳求压力,乃至能够不断地添加从节点。可是读写别离架构在添加事务代码复杂度的一起,也会添加Redis集群架构复杂度。不只要为多个从节点提供转发层(如Proxy,LVS等)来实现负载均衡,还要考虑从节点数量显著添加后带来毛病率添加的问题。Redis集群架构改变会为监控、运维、毛病处理带来了更大的应战。