技能提升(1)-- 缓存专题

开头老规矩,手动推荐一波了,欢迎大家关注我的公众号“风云编程录”,感谢大家🙏

今天抽空整理了一下缓存这方面的内容,缓存这东西肯定大家都知道,但是完整的涉及缓存方方面面的问题,想必大家肯定考虑的没有那么全面。来吧,让我们一起study一下。

1. 缓存的基本使用方式

1) 应用系统如何使用缓存读写数据

查询过程:

a. 首先从缓存中获取数据,如果获取成功,则返回数据

b. 如果获取失败,则从数据库中获取

c. 从数据库获取完成后,将数据重新写入缓存中

更新过程(也有可能这两个过程顺序相反,都存在相应的问题,后续会介绍):

a. 更新数据库

b. 更新缓存

2) 如何应对缓存空间不足的问题

缓存系统大多使用内存进行存储,内存相对于硬盘空间来说,成本更高,空间也有限,在空间不足的情况下,面对越来越多的数据,如何提升命中率是一个非常重要的问题。

究其本质,主要是以下两个问题:

i. 缓存中的数据存储多久?

ii. 如果空间已满,再来数据需要如何处理?

解决方案:

i. 缓存过期时间

设置有缓存过期时间的数据,在过期时间到达时,将被缓存自动清除

ii. 缓存淘汰策略

就是字面意思,当缓存已满,而且缓存内的数据都没有达到过期时间的时候,采用哪种策略删除缓存。

常见的缓存淘汰策略算法有很多,这里介绍两种比较常用而且好用的策略:

LRU(Least recently used,最近最少使用)

LRU算法会将近期最不会访问的数据(即长时间没有被使用的数据)淘汰掉。

LFU(Least Frequently Used 最不经常使用算法)

LFU算法会将一段时间内使用次数最少的数据淘汰掉。

具体这两种算法的实现方式,大家可以参考我CSDN的这篇博文,主要是介绍XXL-JOB中执行器路由选择策略,

https://blog.csdn.net/whq789456/article/details/121282132

其中就有介绍这两种算法具体的实现方式。

3) 查询机制与更新机制带来的挑战

数据查询流程如下:

基于以上查询过程的流程展示,我们可以看到查询过程中可能存在两个比较严重的问题:

如果短时间内出现大量未命中缓存的请求,将发生什么?

如果未命中缓存的请求,在数据库中同样无法查找到数据,又会怎样?

具体这两个问题怎么解决呢?我们继续看后面的介绍

当然多个线程之间更新又有可能发生什么呢?

2. 如何应对缓存读取机制带来的挑战

1) 缓存失效的场景:

a. 缓存穿透

查询的是缓存中不存在,同时数据库中也不存在的数据,即所有查询缓存失效,那么所有的查询将会集中访问数据库,极大的增大数据库压力,降低访问效率,增大了时延,甚至导致数据库崩溃。

这种情况对于系统不一定是威胁,但是对系统有极大的安全影响。

b. 缓存击穿(热点数据,新浪微博某个热点过去,但是大批量请求来)

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

eg. 这种场景可以举一个例子,这些数据常常被称为“热点数据”,比如新浪微博中某一个明星公布恋情的消息,这条消息在缓存中设置了过期时间,如果这条数据在缓存中已经过期,但是仍然有大量客户访问,就会出现这种情况。

c. 缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间或数据尚未加载到缓存中,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而去查数据库。

2) 解决方案:

i. 缓存穿透:

查询的是缓存中不存在,同时数据库中也不存在的数据

a. 空值缓存

对于某条在数据库中也未查询到值的key,也将其放到缓存中,只不过将对应的值置为空。此时再访问,将会从缓存中取出空值并返回。

缺陷:

关注点一:这种情况下,肯定会占用缓存空间 《— 我们可以设置较短过期时间

关注点二:不一致性 《— 我们可以先更新数据库后删除缓存

b. 布隆过滤器

该过滤器由一个位数组(断句是 一个,位数组,也就是数组中只存0和1)和一系列哈希函数组成,能够快速判断出一个元素是否存在于元素池中。布隆过滤器工作在应用查询缓存前,当应用有查询请求时,先去过滤器查看该数据是否存在,如果不存在,说明数据库中没有该数据,直接返回即可;若存在,再去查询缓存,缓存中没有的话才查询数据库,极大地减轻了数据库的压力。

可以使用Google的Guava来实现布隆过滤器,该过滤器相比于空值缓存代码维护更加复杂,但占用缓存的空间较少。适合数据命中不高、数据相对固定的场景

ii. 缓存击穿:

应用大量请求缓存中不存在但数据库中存在的热点数据

预防措施:

预防击穿需要选用合适的缓存淘汰策略,采用LRU(最近最少使用),LFU缓存淘汰算法,来决定哪些缓存该淘汰,能很大程度地在缓存中保留“热点”数据,防止缓存击穿情况的发生。常见的缓存如:redis、ignite、memcache等均提供了LRU 缓存删除策略的选择。

应对方案:采用限流机制

通过加锁(分布式锁,本地同步锁等)的方式保证同一时间只有一个线程能够拿到锁并去访问数据库,其他线程如果没有拿到锁则重新访问缓存看数据是否存在,如果没有存在则继续争抢锁,直到抢到锁或查询到结果为止。

iii.缓存雪崩:

缓存雪崩是指缓存中数据大批量到过期时间,同时伴随大量的查询请求这些数据(和缓存击穿不同的是,缓存击穿指并发查同一条数据)

预防措施:差别失效时间

给不同的缓存设置不同的失效时间,可以使用在一定范围内的随机数,这一也能够在一定程度上避免所有缓存同时失效这一情况的发生,降低缓存雪崩发生的概率。

应对方案:限流机制

缓存设计人员可以通过使用队列的方式来保证同一时间最多只有固定数目的查询请求线程来访问数据库和更新缓存,其他查询请求线程需要在队列中等待。

iv. 特殊的缓存雪崩模式:

新启用缓存引起缓存雪崩问题

缓存雪崩:数据尚未加载到缓存中,同时伴随大量查询请求(常见于缓存初始后或重启后),导致请求直接访问数据库

解决方案:缓存预热

缓存启动后在对外提供服务前,从后端存储介质中导入部分应用使用频率较高的数据,避免突然出现大量请求直接访问后端存储介质。

3. 如何应对缓存更新机制带来的挑战

前两种策略就是直接更新缓存,区别在于先更新数据库,还是先更新缓存

1)先更新缓存,再更新数据库

2)先更新数据库,再更新缓存

eg. 这里以第二种策略为例,先更新数据库,然后更新缓存的方式,介绍这种策略的缺陷。

来了两个写线程,线程A首先完成了写数据库的操作(操作1,2),在该线程没有完成更新缓存操作之前(操作7,8);线程B已经完成了更新数据库以及缓存的操作(操作3,4,5,6)。可以看到最终的处理结果数据库中为线程B的更新值,缓存为线程A的更新值,导致缓存不一致(这个不一致的缓存最起码得保存到这个缓存过期的时候)。

当然,先更新缓存,再更新数据库与这种策略差不多,我这里就不赘述了。

下面处理缓存的方式有了变化,不是采用更新缓存的方式,而是采用删除缓存的策略。

3)先删除缓存再更新数据库

第三种策略先删除缓存再更新数据库解决了策略1和策略2写-写场景中的缓存不一致问题

可以看到采用这种策略,前面一二两种策略,没有解决 写-写 这种场景下,缓存不一致的问题。但是采用第三种策略最终结果只有数据库中会更新数据,缓存会被删除。也就是说最终结果数据库更新,缓存清空,这样也就不存在缓存不一致的情况了,大不了下次读取的时候从数据库中读取。但是第三种策略也不是万能的,在下面这种场景下也会出现问题:

异常场景:

可以看到在 写-读 这种场景下,第三种策略就不能保证缓存一致性了。此时由于写在前,会删除缓存,但是此时写线程还没有完成数据库的更新;这时候读线程读取的时候由于缓存已经清空,会尝试从数据库中加载缓存,此时由于数据库未更新,导致最终缓存为旧数据,而写线程完成更新数据库后,数据库中的数据为新数据。最终导致缓存不一致。

4)先更新数据库再删除缓存

第四种策略先更新数据库再删除缓存部分解决了策略3的数据不一致问题

为了解决第三种策略的不足之处,第四种策略变更了顺序,采用先更新数据库,后删除缓存的策略。这种形式可以一定程度上解决第三种策略的缺陷。具体分析如下:

在写-读的场景下,写线程更新完数据库还没有删除缓存的情况下。

(1)如果在上面图中的第三步如果获取该缓存没有获取到,此时读线程会去获取数据库中的数据,并将数据加载到缓存,此时由于写线程已经更新了数据库,所以读线程加载到缓存中的数据也是最新的。但是随后写线程又会删除缓存。最终结果就是:数据库中已经更新,缓存为空。

(2)如果在上面图中的第三步如果获取该缓存有数据,那此时读线程获取到的缓存数据就是旧数据。随后写线程又会删除缓存。最终结果是:数据库中已经更新,缓存为空,但需要注意到的是此次读线程获取到的是旧数据。也就是说在写线程删除缓存之前并且缓存中数据未到过期时间,这中间读线程获取到的数据都是旧数据。

可以看到第四种策略在写-读这种场景下,部分解决了第三种策略的问题。当然第四种策略还存在一种异常情景。

异常场景

在 读-写这种场景下,在读线程获取到缓存中数据为空,需要更新缓存之前,写线程已经更新了缓存,会导致最终结果读线程将缓存覆盖成旧数据。导致缓存不一致,但是这种情况首先是缓存存在过期时间,过期后就会从数据库中重新弄获取,另外这种场景要求读线程的处理过程要慢于写线程的时间,这种情况几率很小。

综合上面的分析,第四种策略存在两个问题:

1. 先写后读的情况下,可能在删除缓存并且缓存过期之前,读取到的是缓存中的旧数据

2. 先读后写的情况下,在读线程更新缓存之前,写线程已经更新了缓存,会导致缓存被覆盖成旧数据,这种情况几率很小。

分析了这么多种策略,都存在问题,相比于1,2两种策略,3,4策略已经有很大进步,尤其是第四种策略,缓存不一致的情况出现的机率已经很小了。

我们可以看到3,4两种策略最终都是缓存被刷回了老数据,有没有可能删掉这个缓存,进一步减少缓存不一致的概率呢?

5)延迟双删

策略3与策略4的结合体-解决不一致问题

延迟双删策略相比于第四种策略可以看到,经过一段休眠时间后,会在进行一次删除缓存的策略。可以看成是3,4两种策略的结合。而且经过这一步删除缓存的操作,进一步加快了缓存清理的过程,相当于加快了第四种策略中缓存的失效。

延迟双删:先删除缓存再更新数据库,休眠一段时间后再删除缓存

休眠时间:一般来说是1s或1s以内,可使用【业务逻辑读数据库的耗时+几百毫秒】计算

介绍完了这五种策略,可以看到不管采用哪一种形式,都是会出现缓存不一致的情况,并不能完全避免。只是通过特定策略尽可能减少这种缓存不一致的出现。

相比较这五种策略中,第五种策略缓存不一致的出现概率最小,但是设计复杂,其实平常的系统中采用第四种形式几乎就已经可以达到我们的要求,满足第四种策略缓存不一致的场景概率已经很低了。同时需要给大家介绍的是第四种策略就是经常提到的 cache-aside 模式,关于缓存的各种形式,我会在后续其他文章中介绍,这里暂时就挖一个坑吧,哈哈哈哈。。。。

最后还是老规矩,推荐一波:欢迎大家关注我的公众号“风云编程录”,感谢大家🙏

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值