# 缓存失效的场景

图片

我们使用缓存的主要目是提升查询速度和保护数据库等稀缺资源不被占满。而缓存最常见的问题是缓存穿透、缓存击穿和缓存雪崩,在高并发下这三种情况都会有大量请求落到数据库,导致数据库资源占满,引起数据库故障

# 缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而缓存没有命中,导致大量请求直接落到数据库上

想象一下这个情况,如果传入的参数为-1,会是怎么样?这个-1,就是一定不存在的对象,就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的Key,进行攻击

解决方式

# 缓存击穿

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

缓存击穿与缓存穿透的区别在于,缓存击穿的Key是真实存在对应值的

想象一下这个情况,在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上

解决方式

# 缓存雪崩

在高并发下,大量的缓存Key在同一时间集中过期(失效),导致大量的请求落到数据库上

缓存雪崩与缓存击穿的区别在于,缓存击穿针对某1个Key缓存,缓存雪崩则是很多Key

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮

解决方式

# 方案说明

# 缓存Null

应对缓存穿透最有效的方法是直接缓存Null值,但是缓存Null的时间不能太长,否则Null数据长时间得不到更新,也不能太短,否则达不到防止缓存击穿的效果,当Null缓存过期还可以使用限流,缓存预热等手段来防止穿透,具体的Key需要使用特殊的标识,能和真正缓存的数据区分开

# 布隆过滤器拦截

布隆过滤器是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难

使用布隆过滤器,在缓存的基础上,构建布隆过滤器数据结构,在布隆过滤器中存储对应的Key,如果存在,则说明Key对应的值为空

  1. 根据Key查询缓存,如果存在对应的值,直接返回;如果不存在则继续执行
  2. 根据Key查询缓存在布隆过滤器的值,如果存在值,则说明该Key不存在对应的值,直接返回空,如果不存在值,继续向下执行
  3. 查询数据库对应的值,如果存在,则更新到缓存,并返回该值,如果不存在值,则更新缓存到布隆过滤器中,并返回空

# 互斥锁

请求发现缓存不存在后,去数据库查询前,使用分布式锁,保证有且只有一个线程去查询数据库,并更新缓存

# 永不过期

缓存不设置过期时间,等待线程异步更新缓存

# 数据随机

缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生,如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中

# 请求限流

应对缓存穿透,击穿,雪崩的常用方法之一是限流,常见的限流算法有滑动窗口,令牌桶算法和漏桶算法,或者直接使用队列、加锁等

通过限制数据库的每秒请求数,避免数据库挂掉。对于被限流的请求,采用服务降级处理,比如提供默认的值,或者空白值

# 缓存预热

有效应对缓存击穿和缓存雪崩的方式之一是缓存预加载,提前加载好缓存

# 缓存高可用

使用Redis Sentinel等搭建缓存的高可用,避免缓存挂掉无法提供服务的情况,从而降低出现缓存雪崩的情况

# 本地缓存

如果使用本地缓存,即使分布式缓存挂了,也可以将数据库查询的结果缓存到本地,避免后续请求全部达到数据库中。当然引入本地缓存也会有相应的问题,比如本地缓存实时性如何保证。对于这个问题,可以使用消息队列,在数据更新时,发布数据更新的消息,而进程中有相应的消费者消费该消息,从而更新本地缓存;简单点可以通过设置较短的过期时间,请求时从数据库重新拉取

上次更新时间: 2023-12-15 03:14:55