## 一、前言 ## > 一些数据我们没有必要每次查询时都去查询到数据库,特别是高 QPS的系统,每次都去查询数据库,对于数据库来说就是灾难。我们使用缓存时,我们的业务系统大概的调用流程如下图: ![][1] ## 二、缓存穿透 ## ### 1、概念 ### 缓存穿透是指查询一个一定不存在的数据,这将导致这个不存在的数据每次请求都要到存储层去查询,在流量大时,可能DB就挂掉了,如果有人利用不存在的key频繁攻击我们的应用,会导致系统无法正常访问。 ### 2、解决思路 ### #### 1)缓存空值 #### 当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间。通常这个时间较短(如5分钟),这个词可能不是热点词汇,较短的缓存时间可以节省内存空间;如果是恶意攻击,之后再访问这个数据将会从缓存中获取,保护了后端数据源; 弊端: - 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 ( 如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。 - 不宜与正常值共用空间,否则当空间不足时,缓存系统的LRU算法可能会先剔除正常值,再剔除空值——这个漏洞可能会受到攻击。 - 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为 5分钟,如果此时存储层添加了这个数据,此段时间会出现缓存层和存储层数据不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。 #### 2)布隆过滤器(Bloom Filter) #### 布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。 Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。 对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。 弊端: Bloom Filter之所以能做到在时间和空间上的效率比较高,是因为牺牲了判断的准确率、删除的便利性 - 存在误判,可能要查到的元素并没有在容器中,但是hash之后得到的k个位置上值都是1。如果bloom filter中存储的是黑名单,那么可以通过建立一个白名单来存储可能会误判的元素。 - 删除困难。一个放入容器的元素映射到bit数组的k个位置上是1,删除的时候不能简单的直接置为0,可能会影响其他元素的判断。可以采用Counting Bloom Filter。 #### 3)代码过滤 #### 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截等; ## 三、缓存击穿 ## ### 1、概念 ### 缓存击穿实际上是缓存雪崩的一个特例,微博有一个热门话题的功能,用户对于热门话题的搜索量往往在一些时刻会大大的高于其他话题,这种我们成为系统的“热点“,由于系统中对这些热点的数据缓存也存在失效时间,在热点的缓存到达失效时间时,此时可能依然会有大量的请求到达系统,没有了缓存层的保护,这些请求同样的会到达db从而可能引起故障。击穿与雪崩的区别即在于击穿是对于特定的热点数据来说,而雪崩是全部数据。 ### 2、解决思路 ### #### 1)热点数据永不过期 #### 从缓存上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的重新构建,将数据刷新,也就是“逻辑”过期. ![][2] 弊端: - 不保证一致性 - 代码复杂度增大(每个value都要维护一个timekey) - 占用一定的内存空间(每个value都要维护一个timekey)。 #### 2)使用互斥锁(mutex key) #### 就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据。 如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。 ![][3] 弊端: - 降低了系统的qps - 代码复杂性增加 - 有出现死锁的可能性, 存在线程池阻塞的风险 - “分布式缓存加锁”通常是一个反模式,如果持有锁的实例不稳定导致没及时释放,就会浪费这个锁,直到锁过期。 #### 3)"提前"使用互斥锁(mutex key) #### 方法同使用互斥锁,但在value内部设置1个超时值(timeout1), timeout1比实际的缓存到期时间(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout并重新从数据库加载数据并设置到cache中。 弊端: - 同使用互斥锁 #### 4)多级缓存 #### 对于热点数据进行二级缓存,且过期时间错开,则请求基本不会直接击穿缓存层到达数据库。 ## 四、缓存雪崩 ## ### 1、概念 ### 缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了、大量热点缓存同时过期导致大量缓存击穿,会有大量的请求进来直接打到DB上面。 ### 2、解决思路 ### 缓存击穿中的解决思路在缓存雪崩同样适用,但是互斥锁在大量热点缓存同时过期时需要同时大量构建新的缓存,可能会造成服务器性能问题。 #### 1)设置不同的失效时间 #### 为了避免这些热点的数据集中失效,那么我们在设置缓存过期时间的时候,我们让他们失效的时间错开。比如在一个基础的时间上加上或者减去一个范围内的随机值。 #### 2)redis高可用 #### 优化redis性能,搭建集群 #### 3)数据预热 #### 可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。 #### 4)限流降级 #### 限流算法 - 计数 - 滑动窗口 - 令牌桶Token Bucket - 漏桶 leaky bucket 限流组件设置限流数值,超出的请求会走开发好的降级组件,返回配置好的默认值。 #### 5)排队加锁 #### 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。 [1]: https://gitee.com/pgmerz/blogi/raw/master/art/redis2.png#vwid=577&vhei=412 [2]: https://gitee.com/pgmerz/blogi/raw/master/art/redis3.png#vwid=974&vhei=541 [3]: https://gitee.com/pgmerz/blogi/raw/master/art/redis4.png#vwid=384&vhei=336 Last modification:November 29th, 2020 at 05:10 pm © 允许规范转载 Support 如果觉得我的文章对你有用,请随意赞赏 ×Close Appreciate the author Sweeping payments Pay by AliPay Pay by WeChat
嗯 看了博主的文章犹如 醍醐灌顶 我觉得 我在 服务器处理问题的思维上 有了 飞跃似的进步 NICE 写得好好哟,我要给你生猴子!
boom