|
引言
今天,重新回顾一下缓存击穿这个问题! 之所以写这个文章呢,因为目前网上流传的文章落地性太差(什么布隆过滤器啊,布谷过滤器啊,嗯,你们懂的),其实这类方案并不适合在项目中直接落地。
那么,我们在项目中落地代码的时候,其实只需要一个注解就能解决这些问题,并不需要搞的那么复杂。
本文有一个前提,读者必须是Java栈,且是用Springboot构建自己的项目,如果是go技术栈或者python技术栈的,可能介绍的思路仅供大家参考!
正文目前缺陷
首先,为什么说目前网上流传的方案,落地性差呢,因为都缺乏一个可以和SpringBoot结合起来的真实场景,基本上都脱离了SpringBoot,只站在java这个层级去分析。那问题就来了,现在还有只用SpringMvc,却不用SpringBoot的公司么?因此,本文尝试将该方案和springBoot结合起来,讲一个确实可行,可以落地的方案!
当然,我们先来说说目前在网上流传的几套方案,到底不靠谱在哪里!
(1)布隆过滤器
关于布隆过滤器,我就不介绍太多,这里就理解为是一个过滤器,用于快速检索一个元素是否在一个集合中;那么当一个请求来的时候,快速判断这个请求的key是否在指定集合中!如果在,说明有效,则放行。如果不在,则无效拦截。 至于实现,各大博客也说了用了google提供的
com.google.guavaguava19.0复制代码
这个包里有现成写好的java类给你使用了,当然demo代码我就不贴了,一抓一大把! 当然,似乎看上去完美无瑕!一切都是那么的合适!
然而到这里,我就真的问一句,你们真的用了这个方案了?
我如果猜的没错,应该没几个人遇到过缓存击穿问题~
更何况,证明这个说法的正确性~
该方案最大的一个问题是布隆过滤器不支持反向删除操作,例如你的项目里活跃的key的数量只有1000w个,但是全部key数量有5000w个,那这5000w个key会全部存在布隆过滤器里!
直到某一天,你会发现这个过滤器太拥挤了,误判率太高,不得不进行重建!
so,你们觉得这个做法真的靠谱?
那么布隆过滤器这个说法出自哪里呢? (大家一定很好奇对不对!)
当然是xx机构~~此处保护自己的狗头~~记住,他们为了割韭菜,一定会选择一些看起来极为高端,但是落地巨不靠谱的方案(这也是区分一个机构到底是割韭菜还是真正有水平的标杆,小白不懂,很容易被坑)~~看到这里,真是惭愧,我的第一篇文章也是写这个方案了,但是在落地过程中,发现了不对劲(此处省略一万字的检讨文,烟哥垃圾~~)。
(2)布谷过滤器
那么,为了解决布隆过滤器查询性能弱、空间利用效率低、不支持反向操作等问题,又有一篇文章诞生了,主张用布谷过滤器来解决缓存击穿问题!
但是,神奇的事情来了,基本上所有的文章都在说布谷过滤器多么多么牛逼,却没有任何落地的方案~
记住,我们平时写代码,一定是怎么方便怎么来!再记住,面试是一回事,代码落地是另一回事~
那,真正简便的方案是什么样的呢?来,我们一步步来~
真正方案
假设,你此刻用的是springboot-2.x的版本,你为了能够连接redis,你在pom文件里加入如下依赖
org.Springframework.bootspring-boot-starter-data-Redis复制代码
然后呢,我们修改application.yml
spring:datasource:redis:database: ...host: ...port: ...(省事,不全贴了)复制代码
ok,说到这里,就不得不说一下spring-cache了,Spring3.1之后,引入了注解缓存技术,其本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,通过在既有代码中添加少量自定义的各种annotation,即能够达到使用缓存对象和缓存方法的返回对象的效果。Spring的缓存技术具备相当的灵活性,不仅能够使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存集成。
例如:我们在代码中经常有这么一段逻辑,在目标方法执行前,会根据key先去缓存中查询看是否有数据,有就直接返回缓存中的key对应的value值,不再执行目标方法;没有则执行目标方法,去数据库查询出对应的value,并以键值对的形式存入缓存。
如果我们不使用例如spring-cache的注解框架,你的代码中会充斥着大量冗余代码,而用了该框架后,以@Cacheable注解为例, 该注解在方法上,表示该方法的返回结果是可以缓存的。
也就是说,该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法。
那么,你的代码只需要这么写
@Override@Cacheable("menu")public Menu findById(String id) {Menu menu = this.getById(id);if (menu != null){System.out.println("menu.name = " + menu.getName);return menu;复制代码
在这个例子中,findById 方法与一个名为 menu 的缓存关联起来了。调用该方法时,会检查 menu 缓存,如果缓存中有结果,就不会去执行方法了。
ok,说到这里,其实都是大家懂得东西!!接下来开始我们的主题:如何解决缓存击穿问题!顺便讲讲穿透和雪崩问题!
来来来,我们回忆一下缓存击穿,穿透以及缓存雪崩的概念!
缓存穿透
在高并发下,查询一个不存在的值时,缓存不会被命中,导致大量请求直接落到数据库上,如活动系统里面查询一个不存在的活动。多嘴一句:缓存穿透是指,请求的是缓存和数据库中都没有的数据!
对于缓存穿透问题,有一个很简单的解决方案,就是缓存NULL值~从缓存取不到的数据,在数据库中也没有取到,直接返回空值。
那么spring-Cache中,有一个配置是这样的
spring.cache.redis.cache-null-values=true复制代码
带上该配置后,就可以缓存null值了,值得一提的是,这个缓存时间要设的少一点,例如15秒就够,如果设置过长,会导致正常的缓存也无法使用。
缓存击穿
在高并发下,对一个特定的值进行查询,但是这个时候缓存正好过期了,缓存没有命中,导致大量请求直接落到数据库上,如活动系统里面查询活动信息,但是在活动进行过程中活动缓存突然过期了。多嘴一句:缓存击穿是指,请求的是缓存没有,而数据库中有的数据!
记住,解决击穿的最简单的方法,只有一个,就是限流!至于怎么限,其实可以各显神通!例如其他文章提到的布隆过滤器,布谷过滤器等,不过是限流方式之一而已!甚至,你用一些其他的限流组件也是可以的!
这里就要说spring-cahce的另一个配置了!
在缓存过期之后,如果多个线程同时请求对某个数据的访问,会同时去到数据库,导致数据库瞬间负荷增高。Spring4.3为@Cacheable注解提供了一个新的参数“sync”(boolean类型,缺省为false),当设置它为true时,只有一个线程的请求会去到数据库,其他线程都会等待直到缓存可用。这个设置可以减少对数据库的瞬间并发访问。
看到这里!!这不就是一个限流方案么?
所以解决方法就是,加一个属性sync=true,就行。代码就像下面这样
@Cacheable(cacheNames="menu", sync="true")复制代码
用了该属性后,可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中。
当然,看到这里,一定会有人和我抬杠!他的问题是这样的!
你这个只是针对单机的限流,并不是整体集群的限流!也就是说,假设你的集群搭建了3000个pod,最差的情况下就是,3000个pod上,每个pod都会发起一个请求去数据库查询,照样还是会导致数据库连接数不够用,等等资源问题!
对于这个问题我只能说!少年,但凡你的公司产品达到这种流量规模,此刻你就不会在看我的文章!你此刻关心的问题是:
(1)哎,买深圳湾一号还是深圳湾公馆呢,纠结!(2)昨天美股又跌了,又损失了两套房(3)昨天提前撤单了,又少挣了几万....(省略一万字)复制代码
当然,如果你非要解决,也有办法。spring的aop有套路的,比如@Transactional的Advice是TransactionInterceptor,那么cache也对应对一个CacheInterceptor,我们只要去改CacheInterceptor,这个切面就能解决。在里头做一个分布式锁!伪代码如下
flag := 取分布式锁if flag {走数据库查询,并缓存结果睡眠一段时间,再次尝试获取key的值复制代码
但是,我还是要多嘴提一句,真没必要~~ 记住一句话,立足实际出发~但凡你的业务到了那种级别,是可以做到区域部署的,完全可以规避开这类问题。
缓存雪崩
在高并发下,大量的缓存key在同一时间失效,导致大量的请求落到数据库上,如活动系统里面同时进行着非常多的活动,但是在某个时间点所有的活动缓存全部过期。
那么针对该问题,最简单的解决方法就是,过期时间加随机值!
但是很麻烦的是,我们在使用@Cacheable注解的时候,原生功能没法直接设置随机过期时间的。
这个老实说,真没啥好方法,只能自己继承RedisCache,对其增强,改写其中的put方法,带上随机时间!
(本文不赘述,自己可以去查阅相关博客,我真的不喜欢写文章贴大量代码,可读性太差了,知道这么个思路就行,出门搜索一下,一堆答案!)
简单聊聊三大问题:缓存穿透、缓存击穿、缓存雪崩
我们就谈谈Redis在实际项目中用作缓存时经常碰到的,也是经常面试的三大问题:缓存穿透、缓存击穿、缓存雪崩,以及这些问题的常用解决方法。
在介绍这三大问题之前,我们需要先了解Redis作为一个缓存中间件,在项目中是如何工作的。首先看一下在没有缓存中间件的时候的系统数据访问的架构图:
客户端发起一个查询请求的时候,首先去缓存中查询,如果数据在缓存中存在,则直接将缓存中的数据返回给客户端;如果数据在缓存中不存在,则继续查询数据库,如果数据在数据库中存在,则将该数据放入缓存中,并返回给客户端,如果数据在数据库中也不存在,则直接返回null给客户端。
什么是缓存穿透
缓存穿透是指查询一个缓存中和数据库中都不存在的数据,导致每次查询这条数据都会透过缓存,直接查库,最后返回空。当用户使用这条不存在的数据疯狂发起查询请求的时候,对数据库造成的压力就非常大,甚至可能直接挂掉。这种情况的流程就变成下图这样了:
缓存穿透解决方案
解决缓存穿透的方法一般有两种,第一种是缓存空对象,第二种是使用布隆过滤器。
第一种方法比较好理解,就是当数据库中查不到数据的时候,我缓存一个空对象,然后给这个空对象的缓存设置一个过期时间,这样下次再查询该数据的时候,就可以直接从缓存中拿到,从而达到了减小数据库压力的目的。但这种解决方式有两个缺点:(1)需要缓存层提供更多的内存空间来缓存这些空对象,当这种空对象很多的时候,就会浪费更多的内存;(2)会导致缓存层和存储层的数据不一致,即使在缓存空对象时给它设置了一个很短的过期时间,那也会导致这一段时间内的数据不一致问题。
第二种方案是使用布隆过滤器,这是比较推荐的方法。所谓布隆过滤器,就是一种数据结构,它是由一个长度为m bit的位数组与n个hash函数组成的数据结构,位数组中每个元素的初始值都是0。在初始化布隆过滤器时,会先将所有key进行n次hash运算,这样就可以得到n个位置,然后将这n个位置上的元素改为1。这样,就相当于把所有的key保存到了布隆过滤器中了。
举个例子,比如我们一共有3个key,我们对这3个key分别进行3次hash运算,key1经过三次hash运算后的结果分别为2/6/10,那么就把布隆过滤器中下标为2/6/10的元素值更新为1,然后再分别对key2和key3做同样操作,结果如下图:
这样,当客户端查询时,也对查询的key做3次hash运算得到3个位置,然后看布隆过滤器中对应位置元素的值是否为1,如果所有对应位置元素的值都为1,就证明key在库中存在,则继续向下查询;如果3个位置中有任意一个位置的值不为1,那么就证明key在库中不存在,直接返回客户端空即可。如下图:
当客户端查询key4时,key4的3次hash运算中,有一个位置8的值为0,就说明key4在库中不存在,直接返回客户端空即可。
所以,布隆过滤器就相当于一个位于客户端与缓存层中间的拦截器一样,负责判断key是否在集合中存在。如下图:
布隆过滤器的好处就是解决了第一种缓存空值的不足,但布隆过滤器也存在缺陷,首先,它有误判的可能,比如在上面客户端查询key4的图中,假如key4经过3次hash运算得到的位置分别是2/4/6,由于这3个位置的值都是1,所以,布隆过滤器就认为key4在库中存在,进而继续向下查询了。所以,布隆过滤器判断存在的key实际上可能是不存在的,但布隆过滤器判断不存在的key是一定不存在的。它的第二个缺点就是删除元素比较难,比如现在要删除key2这个元素,那么需要将2/7/11三个位置的元素值改为0,但这样就会影响到key1和key3的判断。
什么是缓存击穿
缓存击穿是指当缓存中某个热点数据过期了,在该热点数据重新载入缓存之前,有大量的查询请求穿过缓存,直接查询数据库。这种情况会导致数据库压力瞬间骤增,造成大量请求阻塞,甚至直接挂掉。
缓存击穿解决方案
解决缓存击穿的方法也有两种,第一种是设置key永不过期;第二种是使用分布式锁,保证同一时刻只能有一个查询请求重新加载热点数据到缓存中,这样,其他的线程只需等待该线程运行完毕,即可重新从Redis中获取数据。
第一种方式比较简单,在设置热点key的时候,不给key设置过期时间即可。不过还有另外一种方式也可以达到key不过期的目的,就是正常给key设置过期时间,不过在后台同时启一个定时任务去定时地更新这个缓存。
第二种方式使用了加锁的方式,锁的对象就是key,这样,当大量查询同一个key的请求并发进来时,只能有一个请求获取到锁,然后获取到锁的线程查询数据库,然后将结果放入到缓存中,然后释放锁,此时,其他处于锁等待的请求即可继续执行,由于此时缓存中已经有了数据,所以直接从缓存中获取到数据返回,并不会查询数据库。
什么是缓存雪崩
缓存雪崩是指当缓存中有大量的key在同一时刻过期,或者Redis直接宕机了,导致大量的查询请求全部到达数据库,造成数据库查询压力骤增,甚至直接挂掉。
缓存雪崩解决方案
针对第一种大量key同时过期的情况,解决起来比较简单,只需要将每个key的过期时间打散即可,使它们的失效点尽可能均匀分布。
针对第二种redis发生故障的情况,部署redis时可以使用redis的几种高可用方案部署,部署方法可以参考我之前的文章Redis高可用方案—主从(masterslave)架构、Redis高可用架构—哨兵(sentinel)机制详细介绍、Redis高可用架构—Redis集群(Redis Cluster)详细介绍。
除了上面两种解决方式,还可以使用其他策略,比如设置key永不过期、加分布式锁等。
文末
自此,缓存击穿,穿透,雪崩问题都得到圆满解决~~
来源:http://www.yidianzixun.com/article/0nkVy5FR
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|