“本地缓存”架构设计

前言

 

最近在做的项目其实是对老系统的一个深度改造,在老系统里缓存使用这块感觉有些瑕疵。在老系统里不管是“配置数据”还是“业务数据”都统一使用redis作为缓存。

 

“业务数据”使用redis作为缓存无可厚非,但“配置数据”使用使用redis就感觉不是很妥。

首先:过渡依赖redis,一些开关配置都依赖redis,如果redis服务挂掉整个服务瘫痪;

其次:增加redis服务的存取压力,几乎每个流程都会判断各种开关是否开启,对应的每个请求都会有数次redis读取请求。

最后:性能上也不好(与“本地jvm缓存相比”),读取redis是毫秒级的开销。

 

基于上述原因,决定对缓存结构进行重新梳理,整体采用共享缓存+本地jvm缓存的方式。

 

共享缓存+本地缓存

 

共享缓存:采用redis,如果能读取到缓存直接缓存返回,如果读取不到缓存先读取数据库,再写入redis缓存。

优点:全局共享,无需同步,一次设置,可以在多个jvm实例共享;

      存储量大,可以缓存上百G的数据;

缺点:依赖redis集群基础服务;

      由于存在网络开销,存取速度较慢(相对于本地缓存)

     

 

本地缓存:针对后端应用服务器,本地缓存指的就是jvm内存。

优点:访问数据非常快,纳秒级别(相对于redis的毫秒级别);

      不依赖外部基础服务。

缺点:但容量有限,不能存放过多内容;

      每个jvm实例都会存一份,存在数据冗余;

      修改后不便于同步等问题。

 

结合各自的优缺点,针对不同的业务场景采用不同的缓存方式,可以使系统性能达到最优。

 

共享内存redis的使用不用多说,对于正常的大量的业务数据缓存,基本都会采用redis做为缓存。对于少量配置数据、开关标记、固定的启动参数,可以采用本地缓存,针对不同的数据类型又有几种不同的本地缓存实现方式,初步分三种不同“本地缓存”,如下图所示:

 

 



 

如前所述,本地jvm缓存的难点在于保证实例间的数据同步,以及缓存数据大小的控制。根据不同业务场景,分为三类缓存数据:“配置开关”、“固定参数”、“热点数据”。本地缓存的引入是这次优化的核心,下面分别对三类本地缓存数据的同步和更新策略进行讲解。

 

1、“配置开关”数据

 

所谓“配置开关”指的是系统“降级开关”或者“备用切换开关”,这种类型的配置数据要求必须在线修改,及时生效。要做到这点,需要借助配置管理工具来完成,一般公司内部都有自己的配置管理工具,如果没有推荐使用淘宝开源的配置管理工具diamond。源码地址:https://github.com/takeseem/diamond,申请一个账号,即可下载源码。

关于淘宝diamond具体用法,可以自行查阅相关资料。大概流程如下:



 

diamond的配置以文件为单位,客户端会定期(如每隔15秒)向服务端发起检查请求配置文件内容是否发生变化(通过比较配置内容的md5值),如果变化则拉取配置。

然后解析配置文件,把变更的配置key-value,更新到本地JVM内存。

 

在任意一个server端修改配置后,同步到各个系统会有一个延迟时间(比如15秒),即客户端轮询的间隔时间。可以根据自己业务需要 适当调整这个时间。

 

小结:client端在启动的时候会把最新配置写入到jvm内存,当服务端配置发生变化后,会自动拉取变化的配置,更新jvm内存。修改后的参数会有短暂的延迟。

如果你的业务要求0延迟,最好用nettyserver端和client端建立长链接来实现同步,成本会稍微高一点。

 

2、“固定参数”数据

 

这种配置数据一般不会改变,我们可以认为这类数据是不变的,程序启动时直接读入jvm内存,如果要改变数据就只能重启程序。或者把频繁变化的数据划分为第一类“配置开关”数据。

 

在我们系统里,根据使用方式的不同“固定参数”数据又被划为两种:

a、独立的配置数据,在程序启动时写入一个全局的HashMap(由于是单线程写,不用考虑线程安全问题),在使用时,根据key直接从HashMap get即可。这种方式很容易扩展,但需要一个常量类来维护这些key的名称。

另外你也可以把参数类型分类,对每个类型定义一个枚举类,初始化的时候初始化枚举值,使用的时候直接指定某个枚举值即可。这种方式个人觉得更优雅些。

 

b、用于生产模板对象的数据,比如在我们系统里,创建一个新页面,需要一个页面模板对象作为“骨架”。这个模板对象,系统设计之初就已经确定,并且不会改变。我们以前的做法是把这些配置数据写到数据库。在需要的创建页面时 首先new一个页面对象,再从数据库中查询数据set到对象中。当然这里的配置数据,也可以放到jvm内存里,每次new对象的时候,从内存中获取set到新对象。

 

改进做法:在程序启动时,创建一个“全局模板对象”需要的参数依旧从数据库中查询(或者配置文件)。在需要创建新页面时,直接调用这个“全局模板对象”的clone方法。这种做法相对来说更优雅,前提是需要“全局模板对象”类实现Cloneable 重写clone接口,实现“深度克隆”。关于如果实现“深克隆”可以参考这篇博客:http://moon-walker.iteye.com/blog/2374195

 

3、“热点数据”

 

这里的“热点数据”可以是配置数据,也可以是业务数据。

场景一:如果配置数据太多,全部放到内存,会占用太多内存,但经常使用的数据又很少。

场景二:如果某类业务数据很多,但只有少量的数据会被经常用到。

针对这两种场景 我们通常第一时间想到的是使用redis这类的全局共享缓存,修改数据时清除redis缓存,下次查询直接查库,再同步缓存。

 

但如果这两种场景中的数据几乎都是查询,没有修改,或者说修改后有一定延迟可以接受,这时可以采用,通过LRU算法(淘汰最近最少使用的缓存算法)实现的“本地缓存”会更合理一些。关于LUR“本地缓存”可以自己实现(采用双向链表即可实现),也可以采用本地Ehcache实现。

 

如果redis挂掉

 

回到文章开头的问题,如果核心配置数据也采用redis,一旦redis挂掉,整个系统服务就会崩溃。现在我们来看看如果使用“本地缓存”来存放核心配置数据,如果redis挂掉,怎么做到系统不挂。

 

首先我们在调用redis存取服务时,使用“本地缓存”做个开关,如果redis缓存出现问题,就绕过redis缓存,直接操作数据库,这个道理很简单:



 

 

 

有人会问,不使用redis你的系统抗得住嘛?我们会对系统按照业务模块进行“微服务”拆分成多个子工程,对每个子工程进行限流处理:也就是系统的最大处理能力,我们做压力测试,计算在没有redis缓存的情况下,系统处理的最大并发数,以这个最大并发数作为redis缓存失效情况下的限流依据:



 

1、获取当前支持的最大并发数,这个采用“本地缓存”中的“配置开关”配置,如果redis挂掉,获取非redis缓存模式下系统支持的最大并发数(注意在redis缓存模式下的并发数肯定大很多,提前通过压力测试评估得到)。

2、获取当前系统正在执行的请求并发数,具体怎么获取可以参考另一篇博文:http://moon-walker.iteye.com/blog/2375240 中的限流部分。

3、判断当前正在执行的并发数是否大于步骤1中获取的最大并发数,如果已经大于,则直接返回“限流提示”,防止服务挂掉。

 

通过上述处理可以保证即便是在redis挂掉的情况下,系统依旧可以运行,由于并发处理能力降低,只能支持部分用户在系统里进行操作;另一部分用户,可能会被要求重试。但比起系统直接挂掉,这已经是较好的降级措施了。如果采用redis缓存做配置管理,就达不到上述效果。

 

当然这里指的是后端操作系统,如果是面向全国客户的前端系统,动不动就限流,肯定无法满足需求,对应前端系统可以采用整页静态化,缓存前置等措施,可以参考另一篇博文:http://moon-walker.iteye.com/blog/2332314

猜你喜欢

转载自moon-walker.iteye.com/blog/2390188