先简单介绍一下Redis。
Redis的学术性介绍随处可见,这里用我的理解去试着解释。
Redis,是一种key-value型的存储单元,常被用做缓存,也可以用作数据库,它的查询速度是内存级的,掉电易失,所以一般很少作为数据库使用。一般来讲,当Redis作为缓存使用时,它是位于服务层与数据库之间的。当服务层要查询时,先向Redis进行一次查询,如果得到了结果,则直接返回。如果没有拿到结果,那么才去请求数据库进行查询。所以它不仅提高的检索速度,也减少了数据库的压力。
强调一点,Redis是单进程单实例的。
击穿
击穿 ≠ 穿透 这是肯定的!!
首先看一下什么叫击穿
首先强调,击穿是发生在Redis作为缓存使用的时候。如上面所说,这时用户的查询请求先打到Redis上,如果没有拿到结果再去请求数据库。我们知道Redis中的key是可以设置过期时间的,当到达过期时间时,会有不同的策略去删除key,。关于这部分过期策略的内容我写在下面,跟主线没啥关系,不想了解可以先不看。
Redis的过期策略
我们知道,服务器的内存是有限的,不可能所有的数据都一直存在我们的Redis里,因此我们的Redis要定期地清除冷数据,在清除冷数据的时候有两种策略,分别是LRU和LFU
- LRU:全称叫做Least Recently Used,就是我们最久没有碰过的数据,我们删除这部分数据
- LFU:全称叫做Least Frequently Used,就是我们最近一段时间内碰的最少次数的数据,我们选择删除这部分数据
还有另外一种情况,就是我们为Redis中的key主动设置了过期时间,比如说验证码,对于这部分数据Redis里也有自己的过期策略,分为主动和被动两种。
主动的过期策略就是一种轮询的方式,每过一段时间,我就主动去内存中轮询一遍,看有哪些key过期了,然后将其清除掉
被动的过期策略就是我不主动的去轮询,只有当外界去查询这个key的时候,我才会去判断它的过期时间,如果已经过期,那么我们就返回空值,并删除这个key
我们假设有这样的一种情况,在某个时间点,我们Redis中的某个key值刚刚过期,然后恰巧在这个时候,我们从服务层打过来一个很高的并发量的请求,这些高并发的请求打到我们的Redis上发现并没有找到key,所以我们就继续去请求数据库。
我们知道,我们在做软件架构的时候很重要的一点就是我们要降低数据库的并发访问量,因此我们引入了Redis可以为数据库去承担一部分的并发,现在由于发生了这样的情况,相当于我的请求击穿了我的Redis,直接去打到了我的数据库上,很可能导致我的服务器崩溃。
那么如何去处理这种击穿呢?
嗯,如何处理呢?
首先我们要知道redis中的一个命令,叫set。很简单嘛,就是给一个key去set一个string的value
然后在此之上,set命令可以加参数,nx/xx,即
set [key] [value] [nx|xx] (ps:set [key] [value] nx = setnx [key] [value])
简单的说,set ... nx是只允许新增key,set ... xx是只允许更新key。下面这个图片是我做的例子
可以看到,最开始redis中是没有key的然后我set了k1,又执行了set ... nx的命令想要对k1中的值进行修改,但是没有修改成功,只有新增k2的时候才成功了。随后我使用set ... xx 的命令对k3进行新增,但是也没有成功,只有修改k1的时候才成功了。
所以我们说,setnx只用于新增,setxx只用于修改
setnx set if not exist
setxx set if exist
嗯,还记得我们刚才在说什么吗?对,我们继续说击穿的解决办法。
现在我们在进行的步骤是:一个很高的并发(比如就有1W个请求)打到了Redis上,我们让这些请求首先执行setnx命令。显然只有一个请求能返回ok,其他的都会返回一个空值。然后我们让这个执行了setnx的请求去访问数据库,其他的请求阻塞在Redis层,直到这个访问数据库的请求把数据重新存到Redis里,其他请求继续访问,以此来解决击穿问题。
所以我们可以看到,这里的setnx命令其实就相当于一把锁,一个请求得到了这把锁,其他请求没有得到,所以阻塞在这里。
那么这里其实就存在了一个问题,就是有可能存在死锁。
严格来讲死锁的说法也不对,这种情况主要发生在第一个请求在访问数据库的时候突然挂掉的情况,这样的话就永远没有返回值,那剩余的9999个请求就会永远阻塞在redis里,从而发生一种类似于死锁的情况。
如何去解决这种情况也很简单,就是给我们的这把锁设一个超时时间。
进一步思考,我们的这个超时时间设为多少比较合适?如果这个时间设置的过长,比如设为20s,但是我在第一秒就挂掉了,剩下的19s就相当于是无意义的等待。而如果我的时间比较短的话,在我第一个请求还没有访问完数据库的时候,这个锁就超时了,别的请求就打过来了也会产生问题。
解决情况也很简单,我们开启两个线程,一个线程用于访问数据库,另一个线程用于监控第一个线程,同时更新锁的过期时间。
穿透
穿透也是发生在redis作为缓存使用的情况下。和击穿不同,击穿发生在key过期且并发请求的时候,而穿透是发生在用户进行检索,且检索的是我们系统不存在的记录时。我们下面以商品检索为例。
如果用户查找的是我们系统不存在的商品,那么这个用户的请求就会穿透Redis,访问到数据库。要解决这个问题,我们可以使用布隆过滤器,可以用最小的存储空间将对系统中不存在的商品的检索都阻挡在Redis层级。
简单说一下布隆过滤器的原理。
布隆过滤器主要采用的存储结构是类似于Redis中value类型的bitmap,是一组0/1的列表。在最开始的时候,这个bitmap的值全部为0
然后我们向这个bitmap中注册记录。假设我们的系统中有2个商品,我们对这两件商品分别做n次映射,例如我们可以对商品进行3次哈希运算,映射到我们的bitmap中的3个位,布隆过滤器将这3位置为1。这里需要强调的是,这两件商品做的映射,是有可能映射到同一个位上的。如下图所示.
当用户要进行检索的时候,我们将要检索的数据进行三次相同的映射。如果这件商品是库里存在的商品,那么进行了三次相同的映射,最后映射的结果一定全部为1。如果是库里不存在的商品,那么进行了相同的映射后,就很可能有至少一个位为0。
注意,这里说的是很可能,就是说也有少部分概率三次映射位全都为1。这种情况下请求也会穿透到数据库。但是这种概率一定很小,所以我们说,布隆过滤器是一种概率解决办法,它不可能百分之百的阻挡,但是根据统计,放行的概率会小于1%,从而减少穿透现象的发生。最重要的是,它的成本很低,我们的存储空间仅仅是一个bitmap而已。
布隆过滤器有三种形式,第一种是把整个布隆过滤器都放在客户端,这种是直接把请求拦截在客户端,不向服务器发送;第二种是客户端存着布隆过滤器的算法,而把bitmap存在redis中;第三种则是把整个布隆过滤器都集成在redis中。
布隆过滤器也有它的缺点,比较重要的就是它没有办法进行删除操作,因为在删除的时候,我们虽然可以将要删除的商品映射,但是却不能直接将对应的bitmap位置为0,因为很可能有两件商品都映射在同一个位上。
雪崩
什么是雪崩
雪崩同样发生在redis作为缓存使用的情况下。雪崩和击穿多少有些类似,由于业务逻辑的需求,我们在对redis的key设置过期值的时候,很多时候会以每天的零点(或者其他固定的时间点)作为过期时间。那么我们的redis在零点就会有大量的key失效,从而造成大量请求同时访问数据库。
雪崩的解决办法有两方面,首先如果你的业务逻辑是时点性无关的话,那我们可以通过均匀分布key的过期时间来解决。但很多时候我们的业务逻辑不允许我们这么做,那我们可以利用类似于击穿的解决方案,或者在业务层进行判断,进行零点延时,即在零点附近的请求,我们让其在业务层随机睡眠x个毫秒,从而减少数据库的并发访问。