在上一篇文章你真的了解缓存吗?(1)中,我介绍了引入缓存的利与弊,以及在选择一款缓存中间件时应该注意什么。在这一篇文章中,我们继续介绍在不同的业务场景下,如何进行缓存的选择,具体来说就是缓存的分类,和在使用缓存的过程中可能遇到的问题。
缓存的分类
在我们实际工作中,经常使用的缓存主要分为两类:进程内缓存和分布式缓存。
进程内缓存
进程内缓存主要是指使用和应用程序同一进程的内存来作为缓存的存储空间,因为和应用程序在同一个进程中,所以数据访问的过程不涉及网络开销,访问速度快。但是不足之处在于该进程内缓存的数据,无法被其他节点共享,同时,缓存本身和应用程序使用相同内存资源,两者之间会资源的相互竞争。
现如今大部分应用系统为了应对海量的用户访问,都会采用分布式的方式进行部署,一个应用程序会部署在多个节点上。对于分布式应用场景下,进程内缓存的不足之处就比较明显了,在这种场景下,我们更多的使用分布式缓存。
分布式缓存
分布式缓存,顾名思义,就是将缓存数据存储在多个节点中,实现缓存数据被应用程序的多个节点共享的能力。
而要实现缓存数据被多个节点共享,常用的实现方式有两种:
1.将需要缓存的数据集中存放到独立于应用程序之外第三方系统中,这样应用程序的多个节点都从第三方缓存系统中访问缓存数据。
2.仍然采用进程内缓存的实现方式,但需要将多个节点的进程内缓存数据相互同步,保证各个节点内的缓存数据是相同的,当有缓存数据变更时,会触发数据同步操作。
实现分布式缓存的这两种方式有一个共同特点:都需要涉及到网络IO,相比进程内缓存操作,数据处理效率会低一些。
根据分布式缓存实现的方式不同,分布式缓存可以分为两类:复制式缓存和集中式缓存。
复制式缓存
对于复制式缓存,我们可以将它看作:支持分布式缓存的进程内缓存。
复制式缓存实现过程是这样的:缓存中的所有数据,在应用程序集群中的每个节点里面都存有一个副本,当应用程序需要访问缓存数据时,直接从所在节点的进程中直接获取。
当应用程序需要更新数据时,更新的数据除了写到当前进程的内存中,还需将数据更新同步到其他节点中,数据同步的过程会涉及到大量的网路IO,数据变更成本比较大,当集群中的节点越多,数据更新的成本就越大。
除此之外,因为每个节点都会处理数据的读写,和我们常用的主从架构还不太一样,对主从架构不了解的小伙伴可以参考"如何让mysql存储海量数据"。多节点都支持读写的方式,数据一致性比较难以保证,或者需要耗费比较大的代价才能实现数据一致性。
复制式缓存的代表JbossCache,很多小伙伴应该都没有听说过它吧,是的,为了保证数据一致性,JbossCache的数据写入性能太低了,尤其在大规模集群中,根本无法使用,想进一步了解JbossCache的小伙伴可以参考 https://jbosscache.jboss.org/
集中式缓存
集中式缓存是目前分布式缓存的主流实现方式。集中式缓存将缓存数据集中存储在一个独立于应用集群之外的系统中,说到这里,很多小伙伴就应该会想到我们常用的缓存系统:redis,memcache等。
在使用集中式缓存时,无论数据读写,都需要网络访问,但是好处是,数据读写性能不会随着集群中节点数量的增加而受到的影响。
集中式缓存将数据独立存储的实现方式,在数据存储格式上,通常会采用一种与语言弱相关的存储格式。
也就是说集中式缓存通常可以为其他异构语言的应用程序提供服务。比如说C语言编写的redis可以为Java语言的应用程序提供服务,不同语言的应用程序使用redis提供的对应语言api组件即可,这些api组件会完成不同语言数据格式和redis需要的数据格式的序列化转换。
不过这种独立存储的缺点也很明显,因为在数据格式与语言无关,所以缓存系统在数据格式选择上要尽量通用一些,因此在表达一些像对象这种复杂数据类型的数据时,就比较困难。在redis中只能使用Hash这种类型,来部分模拟对象类型。
而在这一点上,进程内缓存的优势就比较明显了,进程内缓存数据的读写,都不会涉及到序列化和反序列化,这也是数据访问效率比较高的一个原因。
上面我们介绍了缓存的两种分类,主要有进程内缓存和分布式缓存,两种缓存各有优缺点,两者之间更多是互补的关系,而非替换。
我们在实际工作中,经常会将两者配合使用:使用进程内缓存作为一级缓存,分布式缓存作为二级缓存。在数据访问时,如果在一级缓存中查询到结果就直接返回,否则就到二级缓存中查询,再将二级缓存中的结果回填到一级缓存中,如果二级缓存中也无法查到结果的话,那么就回源到最终数据源,然后再将数据源回填到一、二级缓存中。
这种使用进程内缓存和分布式缓存的多级缓存方案,可以充分发挥两者的优势。
具体使用方式如下图所示:
使用缓存可能遇到风险
在之前的文章“什么是缓存和数据库一致性问题?”和“缓存问题“三件套“------- 雪崩,击穿与穿透”,我们有介绍在使用缓存过程中,会经常出现的一些问题,如:缓存穿透,缓存击穿,缓存雪崩和缓存数据不一致的问题。具体的问题以及解决方案,我们上面两篇文章中都有介绍,这里就不过多赘述了。
不过对于缓存一致性的问题,这里简单说一下自己的理解:
缓存系统设计的初衷,是为了缓解cpu和io压力,提升数据访问的性能。在分布式cap原则选择上,更侧重ap,所以从根本上来说,缓存系统自身并没有提供数据一致性的保证,而且为保证数据一致性而做得一些操作,也是违背缓存设计设计初衷的,因为保证数据一致性的操作,会影响数据读写的性能。
目前业内一些常用保证数据一致性的做法,也无法彻底解决数据一致性行问题,只能降低数据不一致的概率,否则的话我们也就不需要设计Paxos这样复杂的共识算法了。
所以,我们在选择一款中间件时,需要结合该中间件设计的初衷和要解决的场景问题,来做选型。在有更多选择的前提下,尽量不要去做在业务层面增强一款中间件功能特性的工作,因为我们之所以选择一款现成的中间件,而不是自己造轮子,主要就是为了提升工作效率,而非增加额外的工作量,否则就得不偿失了。