关于分布式缓存的理解

2023-04-10 14:04:53 浏览数 (1)

关于分布式缓存的理解

分布式缓存首先通过上节课的学习,现在我们已经知道了,服务端缓存可以分为“进程内缓存”和“分布式缓存”两大类。相比缓存数据在进程内存中读写的速度,一旦涉及到了网络访问,那么由网络传输、数据复制、序列化和反序列化等操作所导致的延迟,就要比内存访问高得多。

所以,对于分布式缓存来说,处理与网络有关的操作是影响吞吐量的主要因素,这也是比淘汰策略、扩展功能更重要的关注点。

技术选型只有取舍没有绝对

我们从两个不同的需求场景出发,看看都可以选择哪些分布式缓存方案

复制式缓存与集中式缓存

从访问的角度来说,如果是频繁更新但很少读取的数据,正常是不会有人把它拿去做缓存的,因为这样做没有收益。然后,对于很少更新但频繁读取的数据,理论上更适合做复制式缓存;

而对于更新和读取都较为频繁的数据,理论上就更适合做集中式缓存。所以在这里,我就针对这两种比较通用的缓存形式,给你介绍一下二者之间的差别,以及各自具有代表性的产品。

复制式缓存

对于复制式缓存,你可以看作是“能够支持分布式的进程内缓存”,它的工作原理与 Session 复制类似:缓存中的所有数据,在分布式集群的每个节点里面都存有一份副本,当读取数据时,无需网络访问,直接从当前节点的进程内存中返回,因此理论上可以做到与进程内缓存一样高的读取性能;而当数据发生变化的时候,就必须遵循复制协议,将变更同步到集群的每个节点中,这时,复制性能会随着节点的增加呈现平方级下降,变更数据的代价就会变得十分高昂。

Infinispan提供了一种分布式同步模式。它允许用户配置数据需要复制的副本数量,比如集群中有八个节点,我们可以要求每个数据只保存四份副本,这样就降低了复制数据时的网络负担。

集中式缓存

集中式缓存是目前分布式缓存的主流形式。集中式缓存的读、写都需要网络访问,它的好处是不会随着集群节点数量的增加而产生额外的负担,而坏处自然是读、写都不可能再达到进程内缓存那样的高性能。

集中式缓存还有一个必须提到的关键特点,那就是它与使用缓存的应用分处在独立的进程空间中。

不过现在,因为Redis在集中式缓存中处于统治地位,已经打败了 Memcached 和其他集中式缓存框架,成为了集中式缓存的首选,甚至可以说成为了分布式缓存的首选,几乎到了不用管读取、写入哪种操作更频繁,都可以无脑上 Redis 的程度。

尽管 Redis 最初设计的本意是 NoSQL 数据库,而不是专门用来做缓存的,可今天它确实已经成为许多分布式系统中不可或缺的基础设施,被广泛用作缓存的实现方案。

从数据一致性的角度来说,缓存本身也有集群部署的需求。但我们通常不太会使用缓存来处理追求强一致性的数据。

透明多级缓存

多级缓存

1

尽管多级缓存结合了进程内缓存和分布式缓存的优点,但它的代码侵入性较大,需要由开发者承担多次查询、多次回填的工作,也不便于管理,像是超时、刷新等策略,都要设置多遍,数据更新更是麻烦,很容易会出现各个节点的一级缓存、二级缓存里的数据互相不一致的问题。

所以,我们必须“透明”地解决这些问题,多级缓存才具有实用的价值。

一种常见的设计原则,就是变更以分布式缓存中的数据为准,访问以进程内缓存的数据优先

大致做法是当数据发生变动时,在集群内发送推送通知(简单点的话可以采用 Redis 的 PUB/SUB,求严谨的话可以引入 ZooKeeper 或 Etcd 来处理),让各个节点的一级缓存自动失效掉相应数据。

然后,当访问缓存时,缓存框架提供统一封装好的一、二级缓存联合查询接口,接口外部只查询一次,接口内部自动实现优先查询一级缓存。如果没有获取到数据,就再自动查询二级缓存。

缓存风险

使用缓存的各种常见风险和注意事项,以及应对风险的方法。

缓存穿透(查询不存在数据)

解决办法

对返回为空的 Key 值依然进行缓存

对于恶意攻击,设置一个布隆过滤器来解决,如果布隆过滤器给出的判定结果是请求的数据不存在,那就直接返回即可,连缓存都不必去查。

缓存击穿

热点数据失效,请求就会全部未能命中缓存,打到数据库。

解决办法

加读锁

热点数据手动管理,手工预热

缓存雪崩

大量缓存击穿

解决办法

1 缓存集群 2 不同的加载时间 3 同一时间段内的随机

缓存污染(重要!!!)

缓存中的数据与真实数据源中的数据不一致的现象。

缓存污染多数是因为开发者更新缓存不规范造成的。比如说,你从缓存中获得了某个对象,更新了对象的属性,但最后因为某些原因,比如后续业务发生异常回滚了,最终没有成功写入到数据库,此时缓存的数据是新的,而数据库中的数据是旧的。

解决办法

Cache Aside 模式

  • 读数据时,先读缓存,缓存没有的话,再读数据源,然后将数据放入缓存,再响应请求。
  • 写数据时,先写数据源,然后失效(而不是更新)掉缓存。

在读数据方面,一般不会有什么出错的余地。

但是写数据时,我有必要专门给你强调两点。

一个是先后顺序一定要先数据源后缓存。你试想一下,如果采用先失效缓存后写数据源的顺序,那一定会存在一段时间内缓存已经删除完毕,但数据源还未修改完成的情况。此时新的查询请求到来,缓存未能命中,就会直接流到真实数据源中。

这样,请求读到的数据依然是旧数据,随后又重新回填到缓存中。而当数据源修改完成后,结果就成了数据在数据源中是新的,在缓存中是老的,两者就会有不一致的情况。

二个是应当失效缓存,而不是尝试去更新缓存。这很容易理解,如果去更新缓存,更新过程中数据源又被其他请求再次修改的话,缓存又要面临处理多次赋值的复杂时序问题。所以直接失效缓存,等下次用到该数据时自动回填,期间数据源中的值无论被改了多少次,都不会造成任何影响。

补充:

采用 Cache Aside 模式典型的出错场景,就是如果某个数据是从未被缓存过的,请求会直接流到真实数据源中,如果数据源中的写操作发生在查询请求之后,结果回填到缓存之前,也会出现缓存中回填的内容与数据库的实际数据不一致的情况。但是,出现这种情况的概率实际上是很低的,Cache Aside 模式仍然是以低成本更新缓存,并且获得相对可靠结果的解决方案。

小结

今天这一讲,我着重给你介绍了两种主要的分布式缓存形式,分别是复制式缓存和集中式缓存。其中强调了,在选择使用不同缓存方案的时候,你需要注意对读效率和写效率,以及对访问效率和数据质量之间的权衡。而在实际的应用场景中,你其实可以考虑选择将两种缓存结合使用,构成透明多级缓存,以此达到各取所长的目的。最后,在为系统引入缓存的时候,你还要特别注意可能会出现的风险问题,比如说缓存穿透、缓存击穿、缓存雪崩、缓存污染,等等。如果你对这些可能出现的风险问题有了一定的准备和应对方案,那么可以说,你基本上算是对服务端缓存建立了基本的整体认知了。

0 人点赞