缓存
1. 什么是缓存穿透?
缓存穿透 是指查询一个在数据库和缓存中都不存在的数据。由于缓存无法命中(Miss),这个请求会直接穿透缓存,直达数据库。如果有大量这样的请求同时发生,数据库将不堪重负,甚至可能被压垮。
核心问题: 查询的是一个系统内根本不存在的数据。
------
解决方法:
方案一:缓存空对象(Null Object Caching / 缓存默认值)
这是最常用、最直接的解决方案。
思路:当从数据库查询不到数据时,我们仍然将这个“空结果”(例如 null)进行缓存,并为其设置一个较短的过期时间(比如 1-5 分钟)。
流程:
请求查询 Key。
缓存未命中。
查询数据库,发现数据不存在。
将 Key 和一个表示“空”的特殊值(如 "NULL")写入缓存,并设置过期时间。
后续再有相同 Key 的请求时,缓存直接返回这个空值,而不会访问数据库。
优点:
实现简单,效果立竿见影。
缺点:
内存浪费:如果恶意攻击者构造大量不同的无效Key,会导致缓存中存储大量无用的空对象,占用内存空间。
数据短期不一致:如果在空对象缓存有效期内,数据库里新增了该数据,会导致短期内读到旧的空数据。可以通过设置较短的过期时间或主动删除空缓存来缓解。
方案二:布隆过滤器(Bloom Filter)
这是一个更高效、更专业的内存节约型解决方案。
思路:在缓存之前,加一道“守卫”——布隆过滤器。它是一个概率型数据结构,可以告诉你 “某个元素一定不存在” 或 “可能存在”。
工作原理:
初始化:将所有可能存在的、合法的Key(例如所有有效的用户ID)预先加载到布隆过滤器中。
查询过程:
当一个查询请求过来时,首先去布隆过滤器判断这个Key是否存在。
如果布隆过滤器说 “不存在”,那么这个Key一定不存在于数据库中。此时可以直接返回空结果,无需查询缓存和数据库。
如果布隆过滤器说 “可能存在”,那么再按正常流程去查询缓存和数据库。
优点:
空间效率极高:占用内存非常小,因为它不存储数据本身,只存储数据的“指纹”(哈希位)。
性能极好:查询时间复杂度是 O(k),k是哈希函数的个数,速度非常快。
缺点:
存在误判率:布隆过滤器判断“可能存在”时,有极小的概率是误判(即Key其实不存在)。但这个概率可以通过调整布隆过滤器的参数(如位数组大小、哈希函数数量)来控制到非常低。
删除困难:标准的布隆过滤器不支持删除元素(但有其变种如 Counting Bloom Filter 支持)。
需要数据预热:需要系统在启动或某个时刻,将有效的数据同步到布隆过滤器中。
----------------------------------------------------------------
2.缓存雪崩:
1. 什么是缓存雪崩?
缓存雪崩 是指在某一时刻,大量缓存数据同时失效(或缓存服务宕机),导致所有原本应该访问缓存的请求,瞬间都涌向了数据库。数据库无法承受如此巨大的瞬时压力,从而造成数据库响应缓慢甚至崩溃,进而导致整个系统瘫痪。
核心问题: 大量缓存集中失效或缓存服务不可用。
---------
原因:
场景一:大量缓存Key同时过期
这是最典型的缓存雪崩场景。
原因:在给缓存数据设置过期时间时,如果采用了统一的过期时间(例如,都在凌晨零点过期),那么所有这些缓存都会在同一时刻失效。
例子:一个电商网站,在每天零点刷新优惠券、秒杀商品等信息,如果这些缓存都设置在零点过期,那么零点时所有相关请求都会直接访问数据库。
场景二:Redis集群宕机
原因:Redis服务节点因为断电、网络故障、内存爆满、硬件问题等原因宕机。
例子:主从集群中,主节点宕机且从节点未能成功切换,或者整个Redis集群不可用。
场景三:缓存预热问题
原因:系统刚重启后,缓存是空的(冷启动)。如果此时有大量用户请求涌入,所有请求都会穿透到数据库。
-----------
解决方法:
方案一:差异化过期时间(过期时间加随机值)
方案二:缓存永不过期 + 后台更新
方案三:构建高可用的Redis集群
方案四:服务降级与熔断
方案五:请求限流与队列
--------------------------------------------------
3.缓存击穿:
1. 什么是缓存击穿?
缓存击穿 是指一个热点Key在缓存过期的瞬间,同时有大量的并发请求这个Key。这些请求发现缓存过期后,都会同时去访问数据库,瞬间给数据库带来巨大的压力,就像在缓存屏障上"击穿"了一个洞。
---------
3. 产生缓存击穿的常见场景
热点商品秒杀:某个热门商品参与限时秒杀活动,缓存过期时大量用户同时点击。
爆款新闻/视频:突发事件或热门内容的详情页,缓存失效时被大量用户访问。
明星/网红动态:顶级明星发布动态时,其个人主页缓存失效。
系统定时任务:某些定时刷新的热点数据,在刷新时刻被高频访问。
----------
方案一:互斥锁(Mutex Lock)
这是最常用、最有效的解决方案。
思路:当缓存失效时,不让所有请求都去访问数据库,而是只让一个请求去数据库查询并重建缓存,其他请求等待,直到缓存重建完成
方案二:逻辑过期(永不过期 + 异步更新)
思路:缓存实际上不设置过期时间,而是在value中存储一个逻辑过期时间。当发现数据逻辑过期时,触发异步更新,当前请求仍然返回旧数据。
方案三:热点Key永不过期
思路:对于特别热点的Key,直接设置为永不过期,通过后台任务或数据更新时主动刷新缓存。
-------------------------------------------------------------
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Hexo!