前提
当我们使用缓存时,目标通常有两个:第一,提升响应效率和并发量;第二,减轻数据库的压力。
本文中所提到的这三种场景:缓存穿透、缓存雪崩和缓存击穿的发生,都是因为在某些特殊情况下,缓存失去了预期的功能所致。
当缓存失效或没有抵挡流量,流量直接涌入到数据库,在高并发的情况下,可能直接击溃数据库,导致整个系统奔溃。
缓存穿透
大多数情况,缓存可以减少数据库的查询压力,提升系统性能。
通常流程是:一个请求过来,先查询是否在缓存当中,如果缓存中存在,则直接返回。如果缓存中不存在对应的数据,则检索数据库,如果数据库中存在对应的数据,则更新缓存并返回结果。如果数据库中也不存在对应的数据,则返回或错误。
缓存穿透(cache penetration)是用户访问的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也是去了意义。当高并发或有利用不存在的key频繁攻击时,数据库的压力剧增,甚至奔溃,这就是缓存穿透问题。
缓存穿透发生的场景一般有两类:
- 原来数据是存在,但由于某些原因(误删除、主动清理等)在缓存和数据库层面被删除了,但前端或前置的应用程序依旧保持这些数据;
- 恶意攻击行为,利用不存在的key或者恶意尝试导致产生大量不存在的业务数据请求;
缓存穿透通常有四种解决方案,我们逐一介绍分析:
方案一:缓存空值(null)或默认值
分析业务请求,如果是正常业务请求是发生缓存穿透现象,可针对相应的业务数据,在数据库查询不存在时,将其缓存为空值(null)或默认值。需要注意的是,针对空值的缓存失效时间不宜过长,一般设置为5分钟之内。当数据被写入或更新该key的新数据时,缓存必须同时被刷新,避免数据不一致。
方案二:业务逻辑前置校验
在业务请求的入口处进行数据合法性校验,检查请求参数是否合理、是否包含非法值、是否恶意请求、提前有效阻断非法请求。比如,根据年龄查询时,请求的年龄为-10岁,这显然是不合法的请求参数,直接在参数校验时进行判断返回。
方案三:使用布隆过滤器请求白名单
在写入数据时,使用布隆过滤器进行标记(相当于设置白名单),业务请求发现缓存中无对应数据时,可先通过查询布隆过滤器判断数据是否在白名单内,如果不在白名单内,则直接返回空或失败。
方案四:用户黑名单限制
当发生异常情况时,实时监控访问的对象和数据,分析用户行为,针对故意请求、爬虫或攻击者,进行特定用户的限制;
缓存雪崩
在使用缓存时,通常会对缓存设置过期时间,一方面目的是保持缓存与数据库数据的一致性,另外一方面是减少冷缓存占用过多的内存空间。
但当缓存中大量热点缓存采用了相同的失效时间,就会导致缓存在某一时刻同时失效,请求全部转发到数据库,从而导致数据库压力剧增,甚至宕机。从而形成一系列的连锁反应,造成系统奔溃等情况,这就是缓存雪崩(cache Avalanche)
上面讲到的事热点key同时失败的场景,另外就是由于某些原因导致缓存服务宕机、挂掉或不响应,也同样会导致流量直接转移到数据库。
缓存雪崩的场景通常有两个:
- 大量热点key同时过期
- 缓存服务故障
缓存雪崩的解决方案:
- 通常的解决方案是将key的过期时间后面加上一个
随机数
(比如随机1~5分钟),让key均匀失效 - 考虑用队列或者锁的方式,保证缓存单线程写,但这种方案可能会影响并发量
- 热点数据可以考虑不失效,后台异步更新缓存,适用于不严格要求缓存一致性的场景
- 双key策略,主key设置过期时间,备key不设置过期时间,当主key失效时,直接返回备key值
- 构建缓存高可用集群(针对缓存服务故障情况)
- 当缓存雪崩发生时,服务熔断、限制、降级等措施保障
缓存击穿
缓存雪崩是指只大量热点key同时失效的情况,如果是单个热点key,在不停的扛着大并发,在这个key失效的瞬间,持续的大并发请求就会击穿缓存,直接请求到数据库,好像蛮力击穿一样。这种情况就是缓存击穿(cache Breakdown)
从定义上可以看出,缓存击穿和缓存雪崩很类似,只不过是缓存击穿是一个热点key失效,而缓存雪崩是大量热点key失效,因此,可以将缓存击穿看作是缓存雪崩的一个子集。
缓存击穿的解决方案:
- 使用互斥锁(mutex key),只让一个线程构建缓存,其它线程等待构建缓存执行完毕,重新从缓存中获取数据。单机通过synchronize或lock来处理,分布式环境采用分布式锁
- 热点数据可以考虑不失效,后台异步更新缓存,适用于不严格要求缓存一致性的场景
- “提前”使用互斥锁(mutex key):在value内部设置一个比缓存(redis)过期时间短的过期时间标识,当异步线程发现该值过期时,马上延长内置的这个时间,并重新从数据库加载数据,设置到缓存中去,保证数据的一致性。