前言:
大家都知道计算机性能的瓶颈之一就是IO,为了解决内存与磁盘速度不匹配的问题,产生了缓存,将一些被频繁访问的热点数据放在内存中,随用随取,降低连接到数据库的请求连接和IO存取,避免数据库挂掉。需要注意的是,无论是击穿还是后面谈到的穿透与雪崩,都是在高并发的前提下,比如当缓存中某一个热点key失效时:
击穿
问题起因:
有两个主要原因:
- Key过期
- Key被页面置换淘汰
对于第一个原因是因为在Redis中,Key有过期时间,如果某一个时刻(加入商城做活动,0点开始秒杀)Key失效,那么零点之后都某一个商品查询请求将全部抵达数据库上,导致数据库崩溃。
对于第二个原因,因为内存是有限的的,要时时刻刻缓存新的数据,淘汰旧的数据,所以在一定的页面置换策略(常见页面置换算法图解)中,淘汰数据,如果某些商品做活动之前无人问津,势必会被淘汰。
应对击穿的处理思路
正常处理请求如图:
由于Key过期在所难免,高流量来到Redis时,根据Redis的单线程特性,可以认为任务是在队列里依次执行的,当请求到达Redis发现Key过期时,进行一个操作:设置锁。
这个流程大概如下:
- 请求到达Redis,发现Redis Key 过期,没有获取锁的话回到队列后面排队
- 设置锁,注意,这应该是setnx(),而不是set(),因为可能有其他的线程已经设置了锁。等待取得锁(执行权),否则持续尝试获取锁,等待其他线程释放锁后
- 当前线程取得锁,开始执行代码。去数据库存储数据,请求返回后将DB中的数据存入Redis进行缓存,释放锁返回response。
- 任务队列中的其他任务发现key存在了,直接返回response。
但是引出了一新的问题:拿到锁的请求线程在拿数据时挂掉了怎么办?锁没有被释放,其他请求线程永远也无法取得锁(永远不能执行这部分代码),都在等待这个已经挂掉了的线程释放锁。 解决办法是:
对锁设置一个过期时间,如果达到了过期时间还没有释放锁就自动释放,好,问题又来了,持有锁的线程锁挂了好说,但是如果是线程执行超时呢?也就是在设定的时间里数据没有取出来,但是锁又过期了,常见的是锁过期时间值递增,但是想想不靠谱,因为第一个请求可能超时,如果后面的也超时呢?接连多次超时之后,key的TTL势必特别大了,这样做弊端太多。
另一个思路是:在开启一个线程,进行监控,如果取数据的线程没有挂的话,就适当延迟锁的过期时间。
穿透
问题起因:
穿透主要原因是很多请求都在访问数据库不存在的数据,例如一个卖书的商城一直被请求查询茶叶产品,由于Redis缓存主要是用来缓存热点数据,对于数据库中不存在的数据是没有办法缓存的。这种异常流量就会直接到达数据库并且返回“没有”的查询结果。
应对这种请求,处理办法是队请求加一层过滤器,例如布隆过滤器、增强版过滤器、布谷鸟过滤器,详情见:Redis布隆过滤器与布谷鸟过滤器。
除了不同过滤器,还可以增加一些参数校验,例如数据库数据Id一般都是递增的,如果请求Id=-10这种参数,势必绕过Redis,避免这种情况,可以对用户真实性核验等操作。
雪崩
雪崩和击穿类似,不同的是击穿是一个热点Key某一个时刻生效,而雪崩是大量的热点key在一瞬间失效,网络上很多博客都在强调解决雪崩的策略是随机过期时间,这个非常不准确,举个例子,银行做活动,之前这个利息系数为2%,过了0点系数要改为3%,这种情况将用户的对应Key改为随机过期吗?如果用的过去的数据,那叫脏数据。
明显不可以,同样存钱,你存到年底利息300W,隔壁才200W,这不得打架啊(开玩笑~)
正确的思路是,首先要看看这个Key过期是不是和时点性有关,时点性无关的话可以随机过期时间解决。如果是时点性行管的话,例如刚刚说的银行某天改变某系数,那么就要利用强依赖击穿方案,策略是先过去的线程更新一下所有的Key。
在后台更新热点Key的同时,业务层将进来的请求延时一下,例如短暂的睡几秒或者几毫秒,给后面的更新热点Key分散压力。