文章目录
- 概述
- 一、缓存穿透
- 1.1 缓存穿透是什么
- 1.2 解决方案
- 二、缓存击穿
- 2.1 缓存击穿是什么
- 2.2 解决方案
- 三、缓存雪崩
- 3.1 缓存雪崩是什么
- 3.2 解决方案
- 四、拓展
- 4.1 缓存预热
- 4.2 缓存降级
- 五、结语
把今天最好的表现当作明天最新的起点…….~
概述
在实际的业务场景中,Redis 一般和其他数据库搭配使用,比如和关系型数据库 MySQL 配合使用,用来减轻后端数据库的压力。Redis 会把 MySQL 中经常被查询的数据缓存起来,比如热点数据,这样当用户来访问的时候,就不需要到 MySQL 中去查询,而是直接获取 Redis 中的缓存数据,从而降低了后端数据库的读取压力。如果说用户查询的数据在 Redis 没有找到,那么用户的查询请求就会被转到 MySQL 数据库。当 MySQL 将查询到的数据返回给客户端时,同时也会将数据缓存到 Redis 中,这样用户再次读取时,就可以直接从 Redis 中获取数据。流程图如下所示:
在使用 Redis 作为缓存数据库的过程中,有时也会遇到一些棘手问题,比如常见缓存穿透、缓存击穿和缓存雪崩等问题,如下图所示。本文中将对这些问题做简单地说明,并且提供有效的解决方案。
一、缓存穿透
1.1 缓存穿透是什么
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,那么就去数据库去查找。当用户查询某个数据时,Redis 中不存在该数据,也就是缓存没有命中,此时查询请求就会转向数据库,结果发现数据库中也不存在该数据,数据库只能返回一个空对象(相当于进行了两次无用的查询)。用户拿不到数据时,就会一直发请求查询数据库,这样会对数据库的访问造成很大的压力。如果这种类请求非常多,或者用户利用这种请求进行恶意攻击,就会给数据库造成很大压力,甚至于崩溃,这种现象就叫缓存穿透。
这种现象的原因其实很好理解,当客户端访问不存在的数据时,先请求 Redis,但是此时 Redis 中并没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库。我们都知道数据库能够承载的并发不如 Redis 这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库。
1.2 解决方案
简单的解决方案就是当Redis、数据库中都没有值返回空对象时, 可以在 Redis 中存放一个空值,同时为其设置一个过期时间。这样,当用户再次发起相同请求访问这个不存在的数据,那么就会从缓存中拿到一个空对象,用户的请求被阻断在了缓存层。这样就可以减少重复查询空值引起的系统压力增大,从而从而保护了后端数据库。示例代码如下:
private String queryMessager(String key){
// 从缓存中获取数据
String message = getFromCache(key);
// 如果缓存中没有 从数据库中查找
if(StringUtils.isBlank(message)){
message = getFromDb(key);
// 如果数据库中也没有数据 就设置短时间的缓存
if(StringUtils.isBlank(message)){
// 设置缓存时间(缓存的key,缓存的值,失效时间:单位秒)
redisClient.setNxEx(key,null,60);
} else {
redisClient.setNxEx(key,message,1800);
}
}
return message;
}
这种做法虽然优化了缓存穿透问题,但也存在一些问题。虽然请求进不了数据库,但是会占用 Redis 的缓存空间。而大量的空缓存导致资源的浪费,也有可能导致 Redis 和数据库中的数据不一致。
二、缓存击穿
2.1 缓存击穿是什么
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。比如某个热点数据,它无时无刻都在接受大量的并发访问,如果在某一时刻忽然过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,导致大量的并发请求直接访问数据库,就像在一个完好无损的桶上凿开了一个洞,引起数据库压力瞬间增大,这种现象被称为缓存击穿。
缓存击穿一般出现在高并发系统中,是大量并发用户同时请求到缓存中没有但数据库中有的数据,也就是同时读缓存没读到数据,又同时去数据库去取数据。由于请大量请求同时过来,来不及更新缓存就全部打到数据库那边,引起数据库压力瞬间增大。
2.2 解决方案
- 将热点数据设置加上互斥锁
-
此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。当第一个数据库查询请求发起后,就将缓存中该数据上锁;此时到达缓存的其他查询请求将无法查询该字段,从而被阻塞等待;当第一个请求完成数据库查询,并将数据更新值缓存后,释放锁;此时其他被阻塞的查询请求将可以直接从缓存中查到该数据。
private ReentrantLock reentrantLock = new ReentrantLock(); public static String getData(String key) throws InterruptedException { // 从 Redis 查询数据 String result = getDataByKey(key); // 参数校验 if (StringUtils.isBlank(result)) { // 获取锁 if (reentrantLock.tryLock()) { // 去数据库查询 result = getDataByDB(key); // 校验 if (StringUtils.isNotBlank(result)) { // 搞进缓存 setDataToKey(key, result); } // 释放锁,正常会在finally中释放 reentrantLock.unlock(); } else { // 稍等一下 Thread.sleep(100L); result = getData(key); } } return result; }
-
当某一个热点数据失效后,只有第一个数据库查询请求发往数据库,其余所有的查询请求均被阻塞,从而保护了数据库。但是,由于采用了互斥锁,其他请求将会阻塞等待,可能会存在死锁和线程池阻塞的风险,此时系统的吞吐量将会下降,这需要结合实际的业务考虑是否允许这么做。
-
- 将热点数据设置为永远不过期
-
当向缓存中存储这些数据的时候,可以将他们的缓存失效时间错开,这样能够避免同时失效。如在一个基础时间上加/减一个随机数,从而将这些缓存的失效时间错开。
private void setRandomTimeForReidsKey(String redisKey,String value){ //随机函数 Random rand = new Random(); //随机获取30分钟内(30*60)的随机数 int times = rand.nextInt(1800); //设置缓存时间(缓存的key,缓存的值,失效时间:单位秒) redisClient.setNxEx(redisKey,value,times); }
-
这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
-
三、缓存雪崩
3.1 缓存雪崩是什么
通常,为了保证 Redis 中的数据与数据库中的数据一致性,通常会给 Redis 里的数据设置过期时间。当缓存数据过期后,用户访问的数据如果不在 Redis 里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到 Redis 里,这样后续请求都可以直接命中缓存。
但当Redis 故障宕机或者缓存中大批量的数据同一时间过期(失效),而此时数据访问量又非常大,无法在 Redis 中处理,于是全部直接访问数据库,从而导致数据库压力突然暴增,严重时甚至可能导致数据库崩溃。就像雪崩一样,引发一系列连锁效应,从而波及整个系统崩溃,这种现象被称为缓存雪崩。如下图所示:
假设当时每秒6000个请求,本来缓存在可以扛住每秒5000个请求,但是缓存当时所有的Key都失效了。此时1秒6000个请求全部落数据库,数据库必然扛不住,可能DBA都没反应过来就直接挂了,即便是重启数据库,但是数据库立马又被新的流量给打死了。以秒杀系统为例,图示说明:
它和缓存击穿不同,缓存击穿是在并发量特别大时,某一个热点 key 突然过期,而缓存雪崩则是大量的 key 同时过期,因此它们根本不是一个量级。
3.2 解决方案
出现上述情况的常见原因主要有以下两点:
- 大量缓存数据同时过期,导致本应请求到缓存的需重新从数据库中获取数据。
- Redis 本身出现故障,无法处理请求,那自然会再请求到数据库那里。
针对上面出现故障的情况,可以从以下几点出发解决:
- 事前:构建高可用的集群,实现主 Redis 实例挂掉后,能有其他从库快速切换为主库,继续提供服务,避免全盘崩溃。
- 事中:在往 Redis 存数据时,可以通过随机、微调、均匀设置等方式设置过期时间,这样可以保证数据不会在同一时间大面积失效。如果事情已经发生了,那就要为了防止数据库被大量的请求搞崩溃,可以采用服务熔断或者请求限流的方法。当然服务熔断相对粗暴一些,停止服务直到redis服务恢复;而请求限流相对温和一些,保证一些请求可以处理,不过还是看具体业务情况选择合适的处理方案。
- 事后:redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
四、拓展
4.1 缓存预热
缓存预热就是系统上线前后,将相关的缓存数据直接加载到缓存系统中去,而不依赖用户。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据,这样可以避免那么系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。根据数据不同量级,可以有以下几种做法:
- 数据量不大:项目启动的时候自动进行加载。
- 数据量较大:后台定时刷新缓存。
- 数据量极大:只针对热点数据进行预加载缓存操作。
4.2 缓存降级
缓存降级是指当缓存失效或缓存服务出现问题时,为了防止缓存服务故障,导致数据库跟着一起发生雪崩问题,所以也不去访问数据库,但因为一些原因,仍然想要保证服务还是基本可用的,虽然肯定会是有损服务。因此,对于不重要的缓存数据,我们可以采取服务降级策略。一般做法有以下两种:
- 直接访问内存部分的数据缓存。
- 直接返回系统设置的默认值。
五、结语
Redis 缓存异常会面临的三个问题:缓存雪崩、击穿和穿透。其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样。
Redis 缓存在互联网中至关重要,可以很大的提升系统效率。 本文介绍的缓存异常以及解决思路有可能不够全面,但也提供相应的解决思路和代码大体实现,希望可以为大家提供一些遇到缓存问题时的解决思路。如果有不足的地方,也请帮忙指出,大家共同进步。