缓存穿透和缓存击穿处理

为了应对越来越大的流量,缓存便成为系统服务必不可少的一部分,但使用缓存就会出现缓存击穿和缓存穿透的威胁。

背景介绍

互联网应用逐步深入到生活的各个角落,为了满足越来越多用户使用互联网应用的需求,几乎所有互联网公司都采用缓存的方案来解决瞬时流量超高,或者长期流量过高的问题。但使用缓存存在风险——缓存穿透和缓存击穿:简单的讲就是如果该数据原本就不存在,那么就会发生缓存穿透;如果缓存内容因为各种原因失效,那么就会发生缓存击穿。

具体一点来说,如果缓存中不存在需要查询的内容,一般情况下需要再深入一层进行查询,一般为不能承受压力的关系型数据库(承压能力为缓存的1%,甚至更低),如果数据库中不存在,则叫做缓存穿透;反之,如果数据库中存在这个数据,则叫做缓存击穿(如果同一时刻大量的缓存失效叫做缓存雪崩,本文暂不讨论该问题)。这种查询在流量不高的情况下,不会出现问题,如果查询数据库的流量过高,尤其是数据库中不存在的情况下,严重时会导致数据库不可用,连带影响使用数据库的其他业务,本业务也有很大的可能性受到影响。

缓存穿透常见的处理方式

1、空值缓存

既然该数据本身就不存在,最简单粗暴的方式就是直接将不存在的值定义为空(视具体业务和缓存的方式定义为null或者””)。具体方式是每次查询完数据库,我们可以将key在缓存中设置对应的值为空,短期内再次查询这个key的时候就不用查询数据库了。

通常的简单做法:

0eca5e258183e4a48c29da9b09407dd2.png

这里需要强调注意:为了系统的最终一致性,这些key必须设置过期时间,或者必须存在更新方式,防止这个key的数据后期真实存在,但改key始终为空,导致数据不一致的情况出现。

这种方式的缺点也十分的明显:如果key数量巨大且分散无任何规律,就会浪费大量缓存空间,并且不能抗住瞬时流量冲击(尤其是遇到恶意的攻击的时候,有可能将缓存空间打爆,影响范围更大),需要额外配置降级开关(查询数据库的开关或者限流),这时本方案就显得没想象的那么美好。针对不能抗住瞬时流量的情况,常见的处理方式是使用计数器,对不存在的key进行计数,当某个key在一定时间达到一定的量级,就查询一次数据库,按照数据库的返回值对key进行缓存。未达指定阈值数量之前,按照商定的空值返回。

故这种解决方案的建议使用场景为:key全集数据数据量级较小,并且完全可预测,可以通过提前填充的方式直接将数据缓存。

2、布隆过滤器(BloomFilter)

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。实际应用中,Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。

 布隆过滤器的原理如下:

67455d33968d9e5908ec22fe303ff957.png

假如“京东”经过hash后占位为618,加入后,如下所示:

9230e4ddaeaae70df0a16ed9d68a472b.png

假如大促经过hash后占位为为678,加入后,如下所示:

78cbede30248b5cc2fb48e1524a75b6d.png

假如某宝hash后占位256,则可以完全判断该key不存在,直接返回null即可。但某某多hash后占位167,则不能判断是否存在,需要进行查库操作进行判断。

这里需要强调注意的是必须使用高效的hash算法,否则这种方式会严重影响系统的性能,建议的算法包括MurmurHash、Fnv的稳定高效的算法。

使用过程中,通常把有数据的key都放到BloomFilter中,每次查询的时候都先去BloomFilter判断,如果没有就直接返回空。由于布隆过滤器不支持删除操作(具体结合上图推算删除一个就可以得知),对于删除的key,查询就会经过BloomFilter然后查询缓存再查询数据库,所以BloomFilter建议结合缓存空值用,对于删除的key,可以在缓存中缓存空。(当然有同学自行实现了可删除的布隆过滤器——Counting Bloom Filter,原理较为简单,本文不做具体分析,个人感觉将简单的问题复杂化了)。

同样它也不支持扩容操作,这就要求布隆过滤器建立初期必须进行严格的推算,确保后期不需要扩容,否则重建布隆过滤器的成本可能会超乎想象。具体的推算公式如下(具体的推算过程非本文重点,请各位自行查找):

4a78afae3613f2fa5a3af89df480bc5e.png

k 为哈希函数的个数,m 为布隆过滤器的长度,n 为插入元素的个数(需要处理的数据个数),p 为误报率。

布隆过滤器的使用场景受key的状态限制,如果key是动态无规律的,不建议使用该方式。作者使用布隆过滤器作为用户是否参与活动的过滤,布隆过滤器在活动期间最大值为目前的会员数量,完全可控。

考虑真实情况下,缓存的存储空间及性能问题,在真实使用中,为了避免热key和大key的问题,首先对用户标识进行了Hash,首先对hash按照布隆过滤器的数量进行取余,确定使用哪个布隆过滤器,然后使用布隆过滤器。具体情况如下:

7f12cb382c2bd29a7c3d9acc86bfcc8f.png

缓存击穿常见的处理方式

1、互斥锁(mutex key)

这是比较常见的做法,是在缓存失效的时候,不是立即去查询数据库,先抢互斥锁(比如Redis的SETNX一个mutex key),当操作返回成功时(即获取到互斥锁),再进行查询数据库的操作并回设缓存;否则,就重试整个获取缓存的方法或者直接返回空。SETNX,官方的解释是只在键 key 不存在的情况下,将键 key 的值设置为 value 。若键 key 已经存在,则 SETNX 命令不做任何动作。SETNX 是『SET if Not eXists』(如果不存在,则SET)的简写。

但这种方式显然存在问题,Redis的SETNX命令是当key不存在时设置key,但SETNX不能同时完成EXPIRE设置失效时长,不能保证SETNX和EXPIRE的原子性。这会导致SETNX成功但EXPIRE失败时,锁永远不会释放,下面提供了一种可行的代码,供大家讨论:

try {
    Boolean snx =redisClient.setNX(redisKey, value);
     if (snx && timeout>0){
         boolean flag = redisClient.expire(redisKey, timeout, unit);
        if (!flag){
            redisClient.del(redisKey);
        }
         return flag;
     }
 } catch (Exception e) {
     LOG.error("setnx: key="+redisKey ,e);
 }
 return false;

Redis还是善解人意的,从 2.6.12 起,我们可以使用SET命令完成SETNX和EXPIRE的操作,并且这种操作是原子操作,可以完全替代上述的代码了。

这种简单粗暴的方式有着严格是使用场景(换句话说就是有着严重的缺陷),如果缓存大量失效(缓存雪崩),那么对于数据库是一场灾难;如果数据库查询缓慢,不仅对数据是一场灾难,对于使用该缓存的接口会造成线程阻塞,接口性能又开启了另外一场灾难!一般情况下,这种方式适用于永久缓存的key,或者key偶尔丢失的情况下,其他情况请各位读者慎重考虑或增加其他机制保护数据库和接口(如使用Hystrix限流&降级等)。

2、异步构建缓存

当缓存失效时,不是立刻去查询数据库,而是先创建缓存更新的异步任务,然后直接返回空值。这种做法不会阻塞当前线程,并且对于数据库的压力基本可控,但牺牲了整体数据的一致性。从实际的使用看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,所有查询返回的内容均为空值,但是对于一致性要求不高的互联网功能来说这个还是可以忍受。

ccdf2ce113fa3491b86d0557f512faab.png

总结

综上所述,针对常见的缓存穿透和缓存击穿的问题,各自的优缺点如下:

138bb2265b68f5d78d0a928afa4ec072.png

实战过程中,缓存穿透和缓存击穿是必须考虑的问题,如果基础建设不够稳固(Redis集群),或者缓存时间设置的存在问题,那就必须考虑缓存雪崩(大批量key失效或者集群不可用)的问题,解决的思路大同小异。笔者工作过程中,不同的场景下,会混合使用上面的几种方式,确保系统的稳定和安全。

条条大路通罗马,希望读者能从本文得到一点点启发,开发出符合自己业务的解决方案。

<think>好的,我现在需要帮助用户理解缓存穿透缓存击穿的区别以及它们的解决方案。首先,我得回忆一下这两个概念的定义,然后找出它们之间的不同点,接着整理各自的解决方法。 首先,缓存穿透。我记得缓存穿透是指查询一个不存在的数据,这时候因为缓存中没有,请求直接到达数据库,导致数据库压力过大。这种情况可能是因为恶意攻击,比如故意查询不存在的ID。解决方案可能包括缓存空对象或者使用布隆过滤器来过滤无效请求。 然后是缓存击穿缓存击穿应该是指某个热点数据在缓存过期后,大量请求同时涌入数据库,导致数据库瞬间压力过大。这里的关键点是热点数据突然失效,导致并发请求直接访问数据库。解决方案可能包括设置永不过期、使用互斥锁或者提前更新缓存。 接下来要明确两者的区别。穿透是查询不存在的数据,而击穿是热点数据过期导致的。两者的触发条件影响场景不同,所以解决方案也会有所区别。比如,穿透可能需要布隆过滤器,而击穿可能用互斥锁或者永不过期策略。 然后需要参考用户提供的引用内容。引用[1][2]提到了缓存穿透、雪崩、击穿的问题及解决方案。需要确认这些引用是否提到具体的区别,比如穿透是查询不存在的数据,击穿是热点数据失效。根据引用[2],处理这些问题需要逻辑清晰,明确问题类型后再采取相应措施。 现在需要组织回答结构,先分别解释穿透击穿,然后对比区别,再列出各自的解决方案。确保使用正确的中文表达,符合用户的要求,分点清晰,语言自然。 还要注意用户可能混淆这两个概念,所以需要明确区分它们的触发条件影响。例如,穿透是数据不存在,击穿是数据存在但缓存失效。同时,解决方案要对应问题,比如布隆过滤器处理穿透,互斥锁处理击穿。 最后,生成相关问题,可能需要考虑其他缓存问题如雪崩,或者更深入的技术细节,比如布隆过滤器的实现、互斥锁的应用方式等。</think>### 缓存穿透 vs 缓存击穿的区别及解决方案 #### 1. **缓存穿透** **定义**:当查询的数据既不在缓存中也不在数据库中时,大量请求直接穿透缓存层,导致数据库负载骤增。常见于恶意攻击或无效请求(如查询不存在的ID)。 **特点**: - 数据本身不存在 - 高并发无效请求导致数据库压力 **解决方案**: - **缓存空对象**:对不存在的键设置短时间空值缓存(如`key: null`),避免重复查询数据库。需注意设置过期时间,防止存储过多无效键[^1]。 - **布隆过滤器**:在缓存层前添加布隆过滤器,预先存储所有合法键的哈希值,拦截无效请求[^2]。 --- #### 2. **缓存击穿** **定义**:某个热点数据在缓存过期瞬间,大量并发请求同时访问数据库,导致数据库瞬时压力过大。 **特点**: - 数据存在但缓存过期 - 高并发有效请求集中于同一热点数据 **解决方案**: - **互斥锁(Mutex Lock)**:仅允许第一个请求访问数据库并重建缓存,其他请求等待缓存更新完成。 - **永不过期策略**:对热点数据设置逻辑过期时间,后台异步更新缓存,避免请求直接穿透到数据库。 - **预加载更新**:在缓存过期前主动刷新数据,减少瞬时冲击。 --- #### 3. **核心区别对比** | **维度** | **缓存穿透** | **缓存击穿** | |----------------|---------------------------|---------------------------| | **触发条件** | 数据完全不存在 | 热点数据缓存过期 | | **请求有效性** | 无效请求(如恶意攻击) | 有效请求(高并发访问热点) | | **解决方案重点**| 拦截无效查询(如布隆过滤器)| 控制并发重建缓存(如互斥锁)| --- #### 4. **扩展思考** - **缓存雪崩**(补充):大量缓存同时过期或缓存服务宕机,引发数据库雪崩式压力。可通过随机化过期时间、集群化缓存层解决。 - **混合场景**:实际系统中可能同时发生穿透击穿,需结合多种策略(如布隆过滤器+互斥锁)综合防御。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值