“本地缓存”架构设计

标签: 缓存 架构 设计 | 发表时间:2017-08-17 11:16 | 作者:
出处:http://www.iteye.com

前言

 

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

 

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

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

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

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

 

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

 

共享缓存+本地缓存

 

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

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

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

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

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

     

 

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

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

      不依赖外部基础服务。

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

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

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

 

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

 

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

 

 



 

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

 

1、“配置开关”数据

 

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

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



 

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

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

 

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

 

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

如果你的业务要求0延迟,最好用netty在server端和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



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [缓存 架构 设计] 推荐:

“本地缓存”架构设计

- - ITeye博客
最近在做的项目其实是对老系统的一个深度改造,在老系统里缓存使用这块感觉有些瑕疵. 在老系统里不管是“配置数据”还是“业务数据”都统一使用redis作为缓存. “业务数据”使用redis作为缓存无可厚非,但“配置数据”使用使用redis就感觉不是很妥. 首先:过渡依赖redis,一些开关配置都依赖redis,如果redis服务挂掉整个服务瘫痪;.

缓存设计的一些思考

- Sepher - NOSQL Notes
互联网架构中缓存无处不在,某厂牛人曾经说过:”缓存就像清凉油,哪里不舒服,抹一下就好了”. 高品质的存储容量小,价格高;低品质存储容量大,价格低,缓存的目的就在于”扩充”高品质存储的容量. 缓存的技术点包括内存管理和替换算法. LRU是使用最多的替换算法,每次淘汰最久没有使用的元素. LRU缓存实现分为两个部分:Hash表和LRU链表,Hash表用于查找缓存中的元素,LRU链表用于淘汰.

Web应用的缓存设计模式

- - robbin的自言自语
从10年前的2003年开始,在Web应用领域,ORM(对象-关系映射)框架就开始逐渐普及,并且流行开来,其中最广为人知的就是Java的开源ORM框架Hibernate,后来Hibernate也成为了EJB3的实现框架;2005年以后,ORM开始普及到其他编程语言领域,其中最有名气的是Ruby on rails框架的ORM - ActiveRecord.

软件架构设计

- - 企业架构 - ITeye博客
软件架构设计尚没有万灵的方法论支持,还是个非常新兴的行业,给出个人理解的行业软件架构设计过程,受个人水平有限,仅供参考:. 1.业务分析:针对目标行业的业务战略、蓝图、业务功能及流程进行分析,提出其中部分功能可以使用信息化进行处理,通过分析可以得出信息化要解决的问题. 2.解决方案设计:根据业务战略,形成行业信息化解决方案.

架构设计-逻辑层

- - 人月神话的BLOG
知乎看到一个问题,也是当前在软件设计开发中普遍存在的一个问题,如下:. 现在要开发一个业务逻辑比较复杂的项目,但是在网上看了设计模式的思想后感觉自己以前写的东西扩展性都不好,接口定义也不合适,都是一个实体类一个接口,项目施工也感觉不合理,感觉项目施工中应该先集中定义好接口,并完成业务逻辑,然后在具体实现接口,不知道这样想是不是正确.

架构设计和概要设计

- - 人月神话的BLOG
初步再来探讨下架构设计和概要设计的区别和边界问题. 架构设计包括了功能性架构和技术架构设计两个部分的内容,功能性架构解决业务流程和功能问题,而技术架构解决非功能性需求等问题. 两种架构都包括了动态和静态两个方面的内容,对于功能性架构中动态部分为业务流程驱动全局用例,用例驱动的用例实现等;对于技术架构中动态部分为架构运行机制,而静态部分为框架,分层等方面的内容.

缓存的进化之路—Couchbase的分布式架构

- - 午夜咖啡
本文从缓存的演进,分析了Couchbase分布式缓存的架构. 单机时代一切都是美好的,缓存只是为了解决磁盘访问速度问题,大多数本地缓存基本上都是个HashMap.存储型应用内部都会内置一个缓存,复杂度一般不在缓存本身,而在于存储型应用提供的访问方式.(比如mysql缓存的复杂在于sql查询转换成缓存的key-value查询).

高性能网站架构之缓存篇—Redis集群搭建

- - 开源软件 - ITeye博客
1.  Redis Cluster的架构图.          (1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽..          (2)节点的fail是通过集群中超过半数的节点检测失效时才生效..          (3)客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可.

社区讨论:Android的架构设计

- - InfoQ cn
最近,开发者在知乎社区中就Android的架构设计展开了 讨论. 有人问“Android 架构设计的思想与原则是什么. 最近工作中遇到了Android中的权限问题,发现Android确实是开源的,但并不开放,比如权限控管就相当严格,限制做很多事情,这一点得益于Linux内核. 这也勾起来对其架构研究的兴趣,不知到哪位能够深度剖析下Android架构设计的思想与原则.

分层架构设计原则

- - 博客园_首页
通常一个软件系统都包含不同部分互相交互耦合,我们希望设计能够将系统划分为有意义的各个部件,各个部件能够独立的开发、演进、部署. 这时整体性的设计已经无法满足这些挑战,这就需要我们对系统进行合理清晰的划分. 通常我们为待开发的系统定义多个层次,每一层完成独立的功能. 1:系统分为多层,每层完成独立的功能,层内部继续细分子模块,每层能够独立演进、部署.