目录
前言
总所周知计算机的瓶颈之一就是IO,为了解决内存与磁盘速度不匹配的问题,产生了缓存。将热点数据存放在内存中随用随取,不仅可以降低连接到数据库的请求,避免数据库挂掉;还可以减少频繁重复的复杂逻辑结果。需要注意的是,无论是击穿、穿透与雪崩,都是在高并发前提下。
说到Redis,往往更多的场景是被用作系统的缓存,说到缓存,尤其是分布式缓存系统,在实际高并发场景下,稍有不慎,就会造成缓存穿透、缓存击穿和缓存雪崩的问题。
缓存穿透
产生原因
穿透主要原因是很多请求都在访问数据库一定不存在的数据,造成请求将缓存和数据库都穿透的情况。
例如一个卖书的商城一直被请求查询茶叶产品,由于 Redis 缓存主要是用来缓存热点数据,此数据是在数据库上存在的。对于数据库都不存在的数据,是没法缓存的。这种异常流量就会直接到达数据库并且返回 “NULL” 的查询结果。
解决思路
应对这种请求,处理办法有:
接口层实现api限流、防御DDOS、接口频率限制、网关实现黑名单、用户授权、id检查等;
- 规则排除
可以增加一些参数检验。例如数据库数据 id 一般都是递增的,如果请求 id = -10 这种参数,势必绕过Redis。避免这种情况,可以对用户真实性检验等操作。
null
值填充
当缓存穿透时,redis
存入一个类似 null
的值,下次访问则直接缓存返回空,当数据库中存在该数据的值则需要把 redis
存在的 null
值清除并载入新值,此方案不能解决频繁随机不规则的key请求,只适合单个key 随机生成不同key、影响正常使用。
- 一级二级缓存法/布隆过滤器
例如布隆过滤器、增强版布隆过滤器、布谷鸟过滤器(使用布隆过滤器,但是会增加一定的复杂度及存在一定的误判率;)。如下图:
缓存击穿
产生原因:
在高并发的情况下,当一个缓存key过期时,因为访问该key请求较大,多个请求同时发现缓存过期,因此对多个请求同时数据库查询、同时向Redis写入缓存数据,这样会导致数据库的压力非常大;
- 热点Key 过期
在 Redis 中,key 有过期时间。如果某一时刻(淘宝秒杀,双十一零点开始)key 失效,那么零点之后对某个 key 失效的商品的所有请求将会直接打到数据库上,很有可能倒是数据库崩掉,仅而造成整个服务的不可用。
- Key 被内存淘汰机制淘汰
因为内存是有限的,时时刻刻都有新的缓存数据被放到内存中,而旧的数据被淘汰移除内存。如果开启了 Redis 的内存淘汰机制,Key 会存在被所设置的内存淘汰机制所淘汰的情况。
解决思路
正常的处理请求如图:
由于key过期在所难免,高流量来到Redis时,根据Redis的单线程特性,可以认为任务是在队列里依次执行的,当请求到达Redis发现Key过期时,进行一个操作:设置锁。
这个流程大概如下:
请求到达Redis,发现Redis Key过期,查看有没有锁,没有锁的话回到队列后面排队
设置锁,注意,这儿应该是setnx(),而不是set(),因为可能有其他线程已经设置锁了
获取锁,拿到锁了就去数据库取数据,请求返回后释放锁。
但是引出了一个新的问题,如果拿到锁去拿数据的请求然后挂了怎么办?也就是锁没有释放,其他进程都在等锁,解决办法是:
对锁设置一个过期时间,如果到达了过期时间还没释放就自动释放,问题又来了,锁挂了好说,但是如果是锁超时呢?也就是在设定的时间里数据没有取出来,但是锁由过期了,常见的思路是,锁过期时间值递增,但是想想不靠谱,因为第一个请求可能超时,如果后面的也超时呢,接连多次超时之后,锁过期时间值势必特别大了,这样做弊端太多。
另外一个思路是,再开启一个线程,进行监控,如果取数据的线程没有挂的话,就适当延迟锁的过期时间。
a、使用分布式锁
保证在分布式情况下,使用分布式锁保证对于每个key同时只允许只有一个线程查询到后端服务,其他没有获取到锁的权限,只需要等待即可;这种高并发压力直接转移到分布式锁上,对分布式锁的压力非常大。获取到锁的请求将数据写入成功到redis中, 通知没有获取锁的请求直接从Redis获取数据即可
b、使用本地锁
使用本地锁与分布式锁机制一样,只不过分布式锁适应于服务集群、本地锁仅限于单个服务使用。
c、软过期
设置热点数据永不过期或者异步延长过期时间;
d、布隆过滤器
e、到期前的续命(在value设置一个比过期时间t0小的过期时间值t1,当t1过期的时候,延长t1并做更新缓存操作。)
缓存雪崩
产生原因
雪崩,和击穿类似,不同的是击穿是一个热点Key某时刻失效,而雪崩是大量的热点Key在一瞬间失效,当大量缓存的过期时间相同时,缓存到达过期时间集体失效或者未加载到内存中,大量请求绕过缓存层直接访问数据库 load 数据,导致数据库频繁 IO,性能下降乃至宕机崩溃。
例如:双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存30分钟。那么到了凌晨零点半的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
网络上很多博客都在强调解决雪崩的策略是随机过期时间,这个非常不准确,举个例子,银行做活动,之前这个利息系数为2%,过了零点系数改为3%,这种情况能将用户的对应的key改为随机过期吗?如果用的过去的数据叫脏数据。
明显不可以,同样存钱,你存到年底利息300万,隔壁才200万,这不得打架啊,开玩笑~
正确的思路是,首先要看看这个Key过期是不是时点性有关,时点性无关的话,可以随机过期时间解决。
如果是时点性有关,例如刚刚说的银行某一天改变某系数,那么就要利用强依赖击穿方案,策略是先过去的线程更新一下所有key。
在后台更新热点key的同时,业务层将进来的请求延时一下,例如短暂的睡几毫秒或者秒,给后面的更新热点key分散压力。
解决
1.缓存的高可用性
缓存层设计成高可用,防止缓存大面积故障。即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如 Redis Sentinel 和 Redis Cluster 都实现了高可用。
2.缓存降级
可以利用ehcache等本地缓存(暂时支持),但主要还是对源服务访问进行限流、资源隔离(熔断)、降级等。
当访问量剧增、服务出现问题仍然需要保证服务还是可用的。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级,这里会涉及到运维的配合。
降级的最终目的是保证核心服务可用,即使是有损的。
比如推荐服务中,很多都是个性化的需求,假如个性化需求不能提供服务了,可以降级补充热点数据,不至于造成前端页面是个大空白。
在进行降级之前要对系统进行梳理,比如:哪些业务是核心(必须保证),哪些业务可以容许暂时不提供服务(利用静态页面替换)等,以及配合服务器核心指标,来后设置整体预案,比如:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
3.Redis备份和快速预热
1)Redis数据备份和恢复
2)快速缓存预热
4.提前演练
最后,建议还是在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,对高可用提前预演,提前发现问题。
1.对不用的数据使用不同的失效时间
2.使用集群化分摊部署我们key
3.使用二级缓存
A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期