redis缓存穿透、缓存击穿、缓存雪崩、热点数据集失效问题

1 引言

当我们在原有的系统中引入缓存机制之后,我们的业务系统大概的调用流程如下图所示:
在这里插入图片描述

  1. 当业务系统发起某一个查询请求时,首先判断缓存中是否有该数据;
  2. 如果缓存中存在,则直接返回数据;
  3. 如果缓存中不存在,则再查询数据库,然后返回数据,并同时将数据缓存起来。

这是我们使用缓存最常见的方式,但是这种方式下会存在一些问题:缓存穿透、缓存击穿、缓存雪崩、热点数据失效

2 缓存穿透

2.1 什么是缓存穿透

缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致查询这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

2.2 缓存穿透的危害

如果存在海量请求去查询一个一定不存在的数据,那么这些海量请求都会落到数据库查询中,数据库压力剧增,可能会导致系统崩溃。

2.3 为什么会发生缓存穿透

发生缓存穿透的原因有很多,一般来说主要是以下两种情况导致的:
1、恶意攻击。故意营造大量不存在的数据请求我们的服务,由于缓存中并不存在这些数据,因此海量请求均落在数据库中,从而可能会导致数据库崩溃。
2、代码逻辑错误。如果代码逻辑错误,对于任意输入最终都会转换成一个查询不存在的数据的查询,那么将会引发缓存穿透的问题。

2.4 缓存穿透的解决方案

下面介绍两种防止缓存穿透的方法。

2.4.1 缓存空数据

之所以发生缓存穿透,是因为缓存中没有存储这些空数据的key,导致这些请求全都转移到数据库的查询上面。

那么,我们可以稍微修改一下业务系统的代码,将数据库查询结果为空的key也存储在缓存中。当后续又出现该key的查询请求时,缓存直接返回null,而无需查询数据库,但是别忘了设置过期时间。

2.4.2 使用BloomFilter

第二种避免缓存穿透的方式即为使用BloomFilter(布隆过滤器)。
它需要在缓存之前再加一道屏障,里面存储目前数据库中存在的所有key,如下图所示:
在这里插入图片描述

  1. 当业务系统发起某个请求的时候,首先去BloomFilter中查询该key是否存在;
  2. 若key不存在,则说明数据库中也不存在该数据,因此缓存和数据库都不需要查了,直接返回null;
  3. 若key存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。

2.4.3 两种方案的比较

这两种方案都能解决缓存穿透的问题,但使用场景却各不相同。

对于一些恶意攻击,查询的key往往各不相同,而且数据特别多。此时,第一种方案就显得提襟见肘了。因为它需要存储所有空数据的key,而这些恶意攻击的key往往各不相同,而且同一个key往往只请求一次。因此即使缓存了这些空数据的key,由于不再使用第二次,因此也起不了保护数据库的作用。

因此,总结如下:
(1)对于空数据的key数量有限、key重复请求概率较高的场景而言,应该选择第一种方案。
(2)对于空数据的key各不相同、key重复请求概率低的场景而言,应该选择第二种方案。

3 缓存击穿

3.1 什么是缓存击穿

缓存击穿是由于同时存在大量的查询请求去查询一个数据库中存在的数据,这往往发生在第一次请求缓存数据或者缓存数据到期的时候。在这个瞬间,并发请求特别多,同时查询缓存中没有读到数据,又同时去数据库中查询这个数据,引起数据库中查询压力瞬间增大。

3.2 缓存击穿的危害

缓存击穿会造成某一个瞬间数据库请求量过大,压力剧增。

3.3 缓存击穿的解决方案

3.3.1 后台刷新

后台定义一个job(定时任务)专门主动更新缓存数据。比如,一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中)。

这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定,cache 粒度较大的业务,key 比较分散的则不太适合,实现起来也比较复杂。但是这种方案,不能解决第一次请求缓存数据时造成的缓存击穿问题。

3.3.2 检查更新

将缓存key的过期时间(绝对时间)一起保存到缓存中(可以拼接,可以添加新字段,可以采用单独的key保存等等)。

在每次执行get操作后,都将get出来的缓存数据的过期时间与当前系统时间做一个对比。如果缓存过期时间 - 当前系统时间 <= 1分钟(自定义的一个值),则主动更新缓存。这样就能保证缓存中的数据始终是最新的(和前一个方案一样,目的就是让数据不过期)。

但是,这种方案在特殊情况下也会有问题。假设缓存过期时间是12:00,而 11:59 到 12:00这 1 分钟时间里恰好没有 get 请求过来,又恰好请求都在 12:00 的时 候高并发过来,那就悲剧了。这时候缓存已经过期了,还是要去数据库中查询的。这种情况比较极端,但并不是没有可能,因为“高并发”也可能是阶段性在某个时间点爆发。

3.3.3 分级缓存

采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。

请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。

这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案可能会造成额外的缓存空间浪费。

3.3.4 加锁

通过加锁机制,当多个线程同时请求缓存数据的时候,利用锁来保证某个时刻只有一个线程请求数据。
方法: (推荐)

static Lock reenLock = new ReentrantLock();

	public List<String> getData() throws InterruptedException {
		List<String> result = new ArrayList<String>();
		// 从缓存读取数据
		result = getDataFromCache();
		if (result.isEmpty()) {
			if (reenLock.tryLock()) {
				try {
					System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
					// 从数据库查询数据
					result = getDataFromDB();
					// 将查询到的数据写入缓存
					setDataToCache(result);
				} finally {
					reenLock.unlock();// 释放锁
				}

			} else {
				result = getDataFromCache();// 先查一下缓存
				if (result.isEmpty()) {
					System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
					Thread.sleep(100);// 小憩一会儿
					return getData();// 重试
				}
			}
		}
		return result;
	}

最后使用互斥锁的方式来实现,可以有效避免前面几种问题。

4 缓存雪崩

4.1 什么是缓存雪崩

缓存雪崩就是当数据未加载到缓存中,或者缓存同一时间大面积的失效,从而导致所有请求都去查数据库,导致数据库CPU和内存负载过高,甚至宕机。

4.2 缓存雪崩产生的场景

1.流量激增:比如异常流量、用户重试导致系统负载升高;
2.缓存刷新:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃;
3.程序有Bug:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;
4.硬件故障:比如宕机,机房断电,光纤被挖断等;
5、数据库严重瓶颈:比如:长事务、sql超时等;
6、线程同步等待:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩;

4.3 缓存雪崩的危害

瞬时增大数据库的压力,数据库会因为无法承受而崩溃,导致服务不可用。

4.4 缓存雪崩的解决方案

应对缓存雪崩没有完美的解决办法,但是可以从事前、事中、事后多方面保证服务可用。

(1)事前
缓存集群设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 主从+哨兵 和 Redis Cluster 都实现了高可用。

(2)事中
可以利用ehcache等本地缓存,但主要还需要对源服务访问进行限流、资源隔离(熔断)、降级等。
当访问量剧增、服务出现问题仍然需要保证服务还是可用的。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
使用 Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑。
降级的最终目的是保证核心服务可用,即使是有损的。

(3)事后
开启Redis持久化机制,尽快恢复缓存集群。一旦重启,就能从磁盘上自动加载数据恢复内存中的数据。
整体方案如下:
在这里插入图片描述

5 热点数据集失效

5.1 什么是热点数据集失效

我们在设置缓存的时候,一般会给缓存设置一个失效的时间,过了这个时间,缓存就失效了。

热点数据也就是访问率比较高的数据,对于一些热点数据集来说,当热点数据集中的数据缓存同时失效后会存在大量的请求到数据库上来,从而可能导致数据库崩溃的情况。

5.2 热点数据集失效的解决方案

解决热点数据集失效的问题可以从两方面来考虑。

5.2.1 设置不同的失效时间

为了避免这些热点数据集中的数据同时失效,导致大量访问请求数据库,我们可以在为每个热点数据设置缓存过期时间的时候,让他们的失效时间错开。

比如,在一个挤出时间之上加上或者减去一个范围内的随机值,目的就是尽量保证热点数据集中的数据不要在摸一个时刻同时过期。

5.2.2 采用互斥锁

结合前面讲到的缓存击穿的情况,在第一个请求去查询数据库的时候对它加一个互斥锁,其余的查询请求都会被阻塞住,直到锁被释放,从而保护数据库不崩溃。

但是也是由于它会阻塞其他的线程,此时系统吞吐量会下降,需要结合实际的业务去考虑是否要这么做。

6 总结

当我们使用redis缓存的时候,并不是简单地去连接它,然后通过set 将缓存数据保存,通过get 获取缓存数据这么简单的操作就完事了。我们在使用redis缓存的同时,还应该考虑一下可能会存在哪些潜在的问题?这些问题在我们的业务场景下会不会发生?如果发生了,我们应该采取什么样的解决方案?

本文通过讲解使用redis缓存的时候需要关注的缓存穿透、缓存击穿、缓存雪崩、热点数据集失效问题,以及这些问题都有哪些解决方案,为大家在今后的代码编写和系统设计中提供参考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值