分布式缓存

背景

在高并发的分布式的系统中,缓存是必不可少的一部分。没有缓存对系统的加速和阻挡大量的请求直接落到系统的底层,系统是很难撑住高并发的冲击,所以分布式系统中缓存的设计是很重要的一环

作用

使用缓存我们得到以下收益:整体是利的

  • 加速读写。因为缓存通常是全内存的,比如Redis、Memcache。对内存的直接读写会比传统的存储层如MySQL,性能好很多。举个例子:同等配置单机Redis QPS可轻松上万,MySQL则只有几千。加速读写之后,响应时间加快,相比之下系统的用户体验能得到更好的提升。
  • 降低后端的负载。缓存一些复杂计算或者耗时得出的结果可以降低后端系统对CPU、IO、线程这些资源的需求,让系统运行在一个相对资源健康的环境。

但随之以来也有一些成本:

  • 数据不一致性:缓存层与存储层的数据存在着一定时间窗口一致,时间窗口与缓存的过期时间更新策略有关。
  • 代码维护成本:加入缓存后,需要同时处理缓存层和存储层的逻辑,增加了开发者维护代码的成本。
  • 运维成本:引入缓存层,比如Redis。为保证高可用,需要做主从,高并发需要做集群。

产品

主要是redis,redis常见问题见redis文章分类

缓存读写策略

Cache Aside Pattern(旁路缓存模式)

Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景
写:

  • 先更新 DB
  • 然后直接删除 cache

读 :

  • 从 cache 中读取数据,读取到就直接返回
  • cache 中读取不到的话,就从 DB 中读取数据返回
  • 再把数据放到 cache 中

Read/Write Through Pattern(读写穿透)

写(Write Through):

  • 先查 cache,cache 中不存在,直接更新 DB
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)

读(Read Through):

  • 从 cache 中读取数据,读取到就直接返回
  • 读取不到的话,先从 DB 加载,写入到 cache 后返回响应

Write Behind Pattern(异步缓存写入)

Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。
但是,两个又有很大的不同:Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。

数据库缓存一致性

不一致场景

我们业务中采用的先更新数据库,再删缓存这种情况不存在并发问题么?
不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
但是这样的情况很少,但数据库读写分离情况下出现不一致的情况会更高:
(1)请求B更新数据库
(2)请求B删除缓存
(3)请求A读数据走从库,此时主从还未同步,A查到旧值
(4)请求A将查到的旧值写入缓存
以上情况都是并发情况下无法避免的,除非读写加锁,如果无法忍受则不适合采用缓存

方案

方案并不是要完全避免数据库缓存一致性的情况,但可以很大程度上降低数据库缓存一致性带来的影响

延迟双删

删除缓存的同时,另起一个线程在一段时间(可以是数据库主从延迟的时间多一些)后再次删除缓存,可以采用的双删方案非常多:

  • JDK延迟线程池ScheduledThreadPoolExecutor
  • MQ消息队列
  • Canal监听+MQ消息队列

缓存问题

缓存穿透

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

解决:
1、对查询结果为空的情况也进行缓存,设置一个较短的过期时间
2、布隆过滤器:有误判率,会将不存在的误判为存在,不建议使用

缓存击穿

某些热点缓存数据突然过期,同一时刻大量请求同时进入DB,对数据库造成极大压力

解决:
1、热点数据不过期
2、后台定时任务刷新
3、分布式锁只允许一个线程请求DB并刷新缓存

缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决:
1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。


image.png

布隆过滤器

如何判断一个元素是不是在一个集合里

如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢。

原理

Bloom Filter 是一种空间效率很高的随机数据结构,Bloom filter 可以看做是对 bit-map 的扩展, 它的原理是:
当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:

如果这些点有任何一个 0,则被检索元素一定不在;
如果都是 1,则被检索元素很可能在。

优点

It tells us that the element either definitely is not in the set or may be in the set.

它的优点是空间效率和查询时间都远远超过一般的算法,布隆过滤器存储空间和插入 / 查询时间都是常数O(k)。另外, 散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。

缺点

但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。
(误判补救方法是:再建立一个小的白名单,存储那些可能被误判的信息。)

另外,一般情况下不能从布隆过滤器中删除元素。

误判率

增加位数组长度、哈希函数数量可减少误判率

你可能感兴趣的:(分布式缓存)