Redis - 缓存穿透、击穿、雪崩的异同点以及解决方案
缓存击穿
缓存击穿是指单个热点数据失效时,针对这个数据的大量请求会穿透到持久层,并发量高了之后,数据库宕机。【定点打击】
解决方案:
1、若缓存数据基本不会发生更新,则可尝试将热点数据设置为永不过期。
2、若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
优点:
1)强一致。互斥锁能够确保在缓存重建过程中,只有一个线程可以访问数据库并更新缓存,这样可以避免多个线程同时读取到过期的缓存数据,从而保证了数据的强一致性。
2)实现相对简单。互斥锁的实现相对简单,不需要复杂的逻辑处理,只需在缓存失效时加锁,更新完毕后释放锁即可。
缺点:
- 吞吐量低。在高并发的场景下,互斥锁可能会导致系统的可用性降低,因为大量的请求可能会因为等待锁而无法及时得到处理。
- 逻辑过期。当一个线程查询某个key的缓存数据时,如果该数据已经处于逻辑过期状态,无论该线程是否成功获取互斥锁,系统都会先返回这个逻辑上过期的数据。这样做的好处是即使在数据更新过程中,也能保证用户能立即得到响应,虽然数据可能是旧的。与此同时,如果某个线程成功获取了互斥锁,这意味着它获得了更新缓存数据的权限。这个线程会创建一个单独的工作线程来负责从数据库检索最新数据、更新缓存,并重置该key的逻辑过期时间。一旦这个过程完成,互斥锁便会被释放。
逻辑过期允许在数据达到过期时间点时主动去刷新缓存中的数据,从而确保数据的更新和准确性。如果没有设置逻辑过期,将失去在查询时更新缓存数据的能力,这将导致数据的实时性和一致性难以得到保障。所以,对于不常变更的数据,可以安心设置为永不过期;而对于频繁更新的数据,采用逻辑过期是更为合适的策略
互斥锁和逻辑过期的使用时机:
- 要求数据的强一致时,就使用互斥锁方案
- 当要求数据更高的吞吐量,并且对数据的一致性要求不高时,就使用逻辑过期的方案
3、若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。
缓存雪崩
大量key同时失效,此时刚好有大量请求进来,直接打到数据库层,造成数据库阻塞甚至宕机。【批量打击】
解决方案:
事前:
1)Redis 高可用,主从+哨兵,Redis cluster(集群),避免全盘崩溃。
2)过期时间基础上增加一个随机值,比如1-5分钟随机,不会导致同一秒内大面积的key失效。
setRedis(Key,value,time + Math.random() * 10000)
事前:缓存预热 ,在上线前将高频率访问的数据从数据库加载到Redis。
事中:本地缓存(内存缓存) + 限流&降级,避免 MySQL 被打死。
事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
缓存穿透
缓存穿透是指查询一个根本不存在的数据(可能由于外部的恶意攻击),缓存层和持久层(DB)都不会命中,如果一直请求攻击且并发量高,数据库宕机。
在日常工作中出于容错的考虑,如果从持久层查不到数据则不写入缓存层,缓存穿透将导致不存在的数据每次请求都要到持久层去查询,失去了缓存保护后端持久的意义。
例如,对用户信息进行了缓存,但恶意攻击者使用不存在的用户id频繁请求接口,导致查询缓存不命中,然后穿透 DB 查询依然不命中,这时会有大量请求穿透缓存访问到 DB。
解决方案:
1、 接口层增加合法性参数校验,不合法的参数直接Return,比如:id 做基础校验,id <=0的直接拦截等。
2、如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它过期时间会很短,最长不超过五分钟,否则数据的实效性会产生很大的问题。
3、采用布隆过滤器拦截,将所有可能存在的数据hash到一个足够大的bitmap中。(会有一定的出错率,但不影响拦截不存在的元素)
布隆过滤器:类似于HashSet,可以快速判断一个元素在集合中是否存在
应用场景:快速判断一个元素是否在某容器内,不存在直接返回。(关键点在于hash算法和容器大小)。
原理:利用高效的数据结构和算法快速判断出这个Key是否在数据库中存在,不存在就直接return,存在就去查了DB刷新KV再return。(当布隆过滤器断言某个键(key)不存在时,这个结论是绝对可靠的;但当它认为某个键存在时,只表示有很高的可能性确实存在,尽管有一定的误判几率。)
优点:占用内存小。布隆过滤器不需要存储具体的数据项,只需要存储数据的哈希值,因此相比存储实际数据,它占用的内存更少
缺点:
1)存在一定的误判情况。布隆过滤器存在一定的误判率,即它可能会错误地认为某个不存在的元素存在(假阳性),尽管可以通过调整参数来降低误判率,但无法完全消除。
2)不可逆性。布隆过滤器不需要存储具体的数据项,只需要存储数据的哈希值,因此相比存储实际数据,它占用的内存更少。
3)数据不一致。布隆过滤器与数据库是两个独立的数据管理实体。可能出现的一种情形是,在数据库成功执行了数据更新之后,当尝试更新布隆过滤器时,网络异常发生,导致新增的数据未能及时写入布隆过滤器中,可能后续一个合理的查询请求,它却被“错误地”拦截。
小结:
可以看出其实不存在一个完美无缺的解决方案,最终选择哪种方案必须依据业务场景来决定。例如,虽然引入布隆过滤器可以解决缓存穿透导致的内存占用问题,但它同时可能带来其它问题。这也正是需要培养的思维方式:每当考虑引入新的技术方栈时,必须思考它能够解决哪些问题,以及可能会带来哪些新问题,并要提前规划好如何应对这些潜在的问题。
总结
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」