`

《开源框架那点事儿16》:缓存相关代码的演变

 
阅读更多

问题引入

上次我参与某个大型项目的优化工作,由于系统要求有比较高的TPS,因此就免不了要使用缓冲。

该项目中用的缓冲比较多,有MemCache,有Redis,有的还需要提供二级缓冲,也就是说应用服务器这层也可以设置一些缓冲。

当然去看相关实现代代码的时候,大致是下面的样子。

  1. public void saveSomeObject(SomeObject someObject){  
  2.   
  3.     MemCacheUtil.put("SomeObject",someObject.getId(),someObject);  
  4.   
  5.     //下面是真实保存对象的代码  
  6.   
  7.    
  8.   
  9. }  
  10.   
  11. public SomeObject getSomeObject(String id){  
  12.   
  13.     SomeObject someObject = MemCacheUtil.get("SomeObject",id);  
  14.   
  15.     if(someObject!=null){  
  16.   
  17.          someObject=//真实的获取对象  
  18.   
  19.          MemCacheUtil.put("SomeObject",someObject.getId(),someObject);  
  20.   
  21.     }  
  22.   
  23.     return someObject;  
  24.   
  25. }  
public void saveSomeObject(SomeObject someObject){

    MemCacheUtil.put("SomeObject",someObject.getId(),someObject);

    //下面是真实保存对象的代码

 

}

public SomeObject getSomeObject(String id){

    SomeObject someObject = MemCacheUtil.get("SomeObject",id);

    if(someObject!=null){

         someObject=//真实的获取对象

         MemCacheUtil.put("SomeObject",someObject.getId(),someObject);

    }

    return someObject;

}


 

很明显与缓冲相关的代码全部是耦合到原来的业务代码当中去的。

后来由于MemCache表现不够稳定,而且MemCache的功能,也可以由Redis完全进行实现,于是就决定从系统中取消MemCache,换成Redis的实现方案,于是就改成如下的样子:

  1. public void saveSomeObject(SomeObject someObject){  
  2.   
  3.     RedisUtil.put("SomeObject",someObject.getId(),someObject);  
  4.   
  5.     //下面是真实保存对象的代码  
  6.   
  7.    
  8.   
  9. }  
  10.   
  11. public SomeObject getSomeObject(String id){  
  12.   
  13.     SomeObject someObject = RedisUtil.get("SomeObject",id);  
  14.   
  15.     if(someObject!=null){  
  16.   
  17.          someObject=//真实的获取对象 <span></span>RedisUtil.put("SomeObject",someObject.getId(),someObject);  
  18.   
  19.     }  
  20.   
  21.     return someObject;  
  22.   
  23. }  
public void saveSomeObject(SomeObject someObject){

    RedisUtil.put("SomeObject",someObject.getId(),someObject);

    //下面是真实保存对象的代码

 

}

public SomeObject getSomeObject(String id){

    SomeObject someObject = RedisUtil.get("SomeObject",id);

    if(someObject!=null){

         someObject=//真实的获取对象 <span></span>RedisUtil.put("SomeObject",someObject.getId(),someObject);

    }

    return someObject;

}


 

这一通改下来,开发人员已经晕头晕脑的了,后来感觉性能还是不够高,这个时候,要把一些数据增加二级缓冲,也就是说,本地缓冲有就取本地,本地没有就取远程缓冲  

于是,上面的代码又是一通改,变成下面这个样子:

  1. public void saveSomeObject(SomeObject someObject){  
  2.   
  3.     LocalCacheUtil.put("SomeObject",someObject.getId(),someObject);  
  4.   
  5.     RedisUtil.put("SomeObject",someObject.getId(),someObject);  
  6.   
  7.     //下面是真实保存对象的代码  
  8.   
  9.    
  10.   
  11. }  
  12.   
  13. public SomeObject getSomeObject(String id){  
  14.   
  15.     SomeObject someObject = LocalCacheUtil.get("SomeObject",id);  
  16.   
  17.     if(someObject!=null){  
  18.   
  19.         return someObject;  
  20.   
  21.     }  
  22.   
  23.     someObject = RedisUtil.get("SomeObject",id);  
  24.   
  25.     if(someObject!=null){  
  26.   
  27.          someObject=//真实的获取对象   
  28.   
  29.          RedisUtil.put("SomeObject",someObject.getId(),someObject);  
  30.   
  31.     }  
  32.   
  33.     return someObject;  
  34.   
  35. }  
public void saveSomeObject(SomeObject someObject){

    LocalCacheUtil.put("SomeObject",someObject.getId(),someObject);

    RedisUtil.put("SomeObject",someObject.getId(),someObject);

    //下面是真实保存对象的代码

 

}

public SomeObject getSomeObject(String id){

    SomeObject someObject = LocalCacheUtil.get("SomeObject",id);

    if(someObject!=null){

        return someObject;

    }

    someObject = RedisUtil.get("SomeObject",id);

    if(someObject!=null){

         someObject=//真实的获取对象 

         RedisUtil.put("SomeObject",someObject.getId(),someObject);

    }

    return someObject;

}


 

但是这个时候就出现一个问题: 

由于在某一时刻修改值的只能是某一台计算机,这个时候,其它的计算机的本地缓冲实际上与远程及数据库中的数据会不一致,这个时候,可以有两种办法实现,一种是利用Redis的请阅发布机制进行数据同步,这种方式,会保证数据能够被及时同步。

另外一种方法就是设置本地缓冲的有效时间比较短,这样,允许在比较短的时间段内出现数据不一致的情况。

不管怎么样,功能是实现了,程序员小伙伴这个时候已经改得眼睛发黑,手指发麻,几乎接近崩溃了。

很明显这种实现方式是不好的,于是项目组又提出了改进意见,能否采用注解方式进行标注,让程序员只要声明就可以?Good idea,于是,又变成了下面的样子:

  1. @Cache(type="SomeObject",parameter="someObject",key="${someObject.id}")  
  2.   
  3. public void saveSomeObject(SomeObject someObject){  
  4.   
  5.     //下面是真实保存对象的代码  
  6.   
  7.    
  8.   
  9. }  
  10.   
  11. @Cache("SomeObject",key="${id}")  
  12.   
  13. public SomeObject getSomeObject(String id){  
  14.   
  15.     SomeObject someObject=//真实的获取  
  16.   
  17.     return someObject;  
  18.   
  19. }  
@Cache(type="SomeObject",parameter="someObject",key="${someObject.id}")

public void saveSomeObject(SomeObject someObject){

    //下面是真实保存对象的代码

 

}

@Cache("SomeObject",key="${id}")

public SomeObject getSomeObject(String id){

    SomeObject someObject=//真实的获取

    return someObject;

}


 

这个时候,程序员们的代码已经非常清爽了,里面不再有与缓冲相关的部分内容,但是引入一个新的问题,就是处理注解的代码怎么写?需要引入容器,比如:Spring,这些Bean必须被容器所托管,如果直接new一个实例,就没有办法用缓冲了。还有一个问题是:程序员的工作量虽然有所节省,但是还是要对程序代码有侵入性,需要引入这些注解,如果要增加超越现有注解的功能,还是需要重新写过这些类,引入其它的注解,修改现有的注解。

所以,上面是个可以接受的方案,但明显还不是很好的方案。

假如有一个程序员火大了,他发出下面的抱怨:“我只管做我的业务,放不放缓冲和我有1毛钱关系么?总因为缓冲的事情让我改来改去,程序改得乱七八糟不说,我的时间,我的工作进度都影响了谁来管?以后和缓冲相关的事情别他妈的来烦我!”,作为架构师的你,你怎么看?最起码,我觉得他是说得非常有道理的。我们再返过头来看看最原始的代码:

  1. public void saveSomeObject(SomeObject someObject){  
  2.   
  3.     //下面是真实保存对象的代码  
  4.   
  5.    
  6.   
  7. }  
  8.   
  9. public SomeObject getSomeObject(String id){  
  10.   
  11.     SomeObject someObject=//真实的获取  
  12.   
  13.     return someObject;  
  14.   
  15. }  
public void saveSomeObject(SomeObject someObject){

    //下面是真实保存对象的代码

 

}

public SomeObject getSomeObject(String id){

    SomeObject someObject=//真实的获取

    return someObject;

}


 

这里是干干净净的业务代码,和缓冲没有一点关系。后来由于性能方面的要求,需要做缓冲,OK,这一点是事实,但是用什么缓冲或怎么缓冲,与程序员确实是没有什么关系的,因此,是不是可以不让程序员参与,就可以优雅的做到添加缓冲功能呢?答案当然是肯定的。 

需求整理

  1. 代码当中,不要体现与缓冲相关的内容,也就是说做不做缓冲及怎么做缓冲不要影响到业务代码
  2. 不管是从容器中取实例还是new实例,都可以同样的起作用,也就是说可以不必依赖具体的容器

解决思路:

放不放缓冲、怎么放缓冲、缓冲有效时间等等,这些内容是在运行期发现存在性能瓶颈,然后提交给程序员来进行优化的。为此,我们设计了一个配置来描述这些缓冲相关的声明。

当然,这个配置文件的结构,可以根据自己所采用的缓冲框架来进行相应的定义。

比如:

  1. <redis-caches>  
  2.   
  3.   <redis-cache type="org.tinygroup.redis.test.UserDao">  
  4.   
  5.      <redis-method method-name="saveUser">  
  6.   
  7.            <redis-expire value="1000"></redis-expire>  
  8.   
  9.            <redis-string type="user" key="${user.id}" paramter-name="user"><redis-string>  
  10.   
  11.      </redis-method>  
  12.   
  13.   </redis-cache>  
  14.   
  15.   <redis-cache type="org.tinygroup.redis.test.UserDao">  
  16.   
  17.      <redis-method method-name="getUser">  
  18.   
  19.            <redis-expire value="1000"></redis-expire>  
  20.   
  21.            <redis-string type="user" key="${id}" ><redis-string>  
  22.   
  23.      </redis-method>  
  24.   
  25.   </redis-cache>  
  26.   
  27. </redis-caches>  
<redis-caches>

  <redis-cache type="org.tinygroup.redis.test.UserDao">

     <redis-method method-name="saveUser">

           <redis-expire value="1000"></redis-expire>

           <redis-string type="user" key="${user.id}" paramter-name="user"><redis-string>

     </redis-method>

  </redis-cache>

  <redis-cache type="org.tinygroup.redis.test.UserDao">

     <redis-method method-name="getUser">

           <redis-expire value="1000"></redis-expire>

           <redis-string type="user" key="${id}" ><redis-string>

     </redis-method>

  </redis-cache>

</redis-caches>


 

我们在实际应用当中,配置比上面的示例更完善,那现在我先讲一下上面的两段配置的含义。

在UserDao的saveUser的时候,会同步的把User数据放到Redis中进行缓冲,缓冲时间为1秒,存放的缓冲数据的类型为user,键值为${user.id},也就是要保存的用户的主健。实际进入到Redis的时候,在Redis中的健值是由上面类型与key的共同组成的。

在调用UserDao的getUser的时候,会先从缓冲中获取类型为user,键值为${id}的数据,如果缓冲中在,则取出并返回,如果缓冲中没有,则从原有业务代码中取出值并放入缓冲,然后返回此对象。

哇,这个时候,就非常爽了,只要通过声明就可以做到对缓冲的处理了,但是一个问题就出来了,如何实现上面的需求呢?

通过配置文件外置,确实做到了对业务代码的0侵入,但是如何为原有业务增加缓冲相当的业务逻辑呢?由于需求2要求可以new,也可以从容器中获取对象实例,因此利用容器AOP解决的跑就被封死了,因此,就得引入字节码的方式来进行解决。

具体实现

写一个基于Maven的缓冲代码处理插件,在编译后增加此处理插件,根据配置文件对原有代码进行扫描并修改其字节码以添加缓冲相关处理逻辑。

现在只要使用Maven进行compile或install就可以自动添加缓冲相关的逻辑到class文件中了。

至此,我们已经分析了缓冲代码直接耦合到代码中,并分析了其中的缺点,最终演化了注解方式,外置配置方式,并简要介绍了实现方法。

具体实现,采用的技术就比较多了,有Maven插件、有Asm、有模板引擎还有Tiny框架的一些基础工程,如:VFS,FileResolver等等。

如果采用Tiny框架,可以直接拿来用,如果不用Tiny框架,可以参照上面的思路做自己的实现。

 


 


 

 

欢迎访问开源技术社区:http://bbs.tinygroup.org。本例涉及的代码和框架资料,将会在社区分享。《自己动手写框架》成员群:228977971,让我们一起动手,了解开源框架的奥秘!

11
5
分享到:
评论
7 楼 fxf-风翔飞 2015-08-17  
j2eetop 写道
fxf-风翔飞 写道
这样的话配置项会不会太多了,不方便管理
最好感觉是用注解,或者对该包做拦截,通过对应的命名规则去做缓存的存取

注解的话会耦合,以后切换缓冲方案不方便。


注解也一样,通过对注解做处理的过程也是可以进行切换的
切换缓冲方案直接换个处理流程就行
6 楼 fxf-风翔飞 2015-08-17  
j2eetop 写道
fxf-风翔飞 写道
这样的话配置项会不会太多了,不方便管理
最好感觉是用注解,或者对该包做拦截,通过对应的命名规则去做缓存的存取

注解的话会耦合,以后切换缓冲方案不方便。


那就做拦截呗,以后切换方案换拦截器就行
5 楼 j2eetop 2015-06-25  
zhangchengtest 写道
我也感觉注解要好点吧

注解的话会耦合,以后切换缓冲方案不方便。
4 楼 j2eetop 2015-06-25  
fxf-风翔飞 写道
这样的话配置项会不会太多了,不方便管理
最好感觉是用注解,或者对该包做拦截,通过对应的命名规则去做缓存的存取

注解的话会耦合,以后切换缓冲方案不方便。
3 楼 zhangchengtest 2015-06-24  
不过使用注解new的话就不能用缓存了 怎么样写缓存代码处理插件
2 楼 zhangchengtest 2015-06-24  
我也感觉注解要好点吧
1 楼 fxf-风翔飞 2015-06-19  
这样的话配置项会不会太多了,不方便管理
最好感觉是用注解,或者对该包做拦截,通过对应的命名规则去做缓存的存取

相关推荐

    开源框架面试题系列集:Spring+SpringMVC+MyBatis.zip

    在IT行业中,开源框架是开发高效、稳定应用的基石,Spring、SpringMVC和MyBatis作为Java领域的三大核心框架,被广泛应用于企业级开发。本资料集合专注于这些框架的面试题,旨在帮助求职者和开发者更好地理解和掌握...

    基于Eclipse的开源框架技术与实战 源代码第18-21章

    在本资源中,“基于Eclipse的开源框架技术与实战 源代码第18-21章”提供了关于使用Eclipse开发和应用开源框架的实践经验。这个资料主要涵盖了四个章节的内容,分别是第18章至第21章,旨在帮助开发者深入理解并熟练...

    第三方开源框架

    在IT行业中,第三方开源框架是开发者们不可或缺的工具,它们为快速构建高效、稳定的应用程序提供了强大支持。本文将深入探讨“第三方开源框架”的概念、重要性以及如何使用,以KJFrameForAndroid为例,展示其在解决...

    58同城开源框架

    在实际应用中,58同城开源框架可能会提供以下关键知识点: 1. **模块化设计**:框架通过模块化的结构,让开发者可以独立地开发、测试和部署各个功能模块,降低了系统的复杂度。 2. **RESTful API**:为了实现前后...

    JAVA开源框架学习文档

    根据提供的文件信息:“JAVA开源框架学习文档”,我们可以深入探讨与JAVA开源框架相关的多个知识点,包括但不限于框架的选择、安装配置、核心概念以及实际应用场景等。由于提供的具体内容为空,本篇文章将基于标题和...

    缓存、缓存算法和缓存框架简介.docx

    本文将深入探讨缓存、缓存算法以及缓存框架。 首先,让我们理解缓存的工作机制。缓存通常是一个数据结构,如哈希表,用于存储频繁访问的数据。在上述描述的面试场景中,Programmer One 使用哈希表实现了一个简单的...

    开源框架面试题系列:Spring+SpringMVC+MyBatis

    在IT行业中,开源框架是构建复杂应用程序的基础,而Spring、SpringMVC和MyBatis作为Java领域的三大核心框架,它们的熟练掌握对于开发者来说至关重要。本系列主要关注这些框架在面试中的常见问题,旨在帮助求职者提升...

    开源框架面试题系列:Spring+SpringMVC+MyBatis.rar

    在IT行业中,Java技术栈是企业级应用开发的主流选择,Spring、SpringMVC和MyBatis这三大开源框架更是核心组件。对于求职者来说,掌握这三个框架的深入理解和使用技巧,是通过技术面试的关键。以下是对这三者进行详细...

    开源框架面试题系列:Spring+SpringMVC+MyBatis.zip

    在IT行业中,Spring、SpringMVC和MyBatis是三个非常重要的开源框架,它们在企业级Java应用开发中占据了核心地位。本压缩包文件提供的面试题系列,旨在帮助求职者和开发者深入理解这三个框架的核心概念、工作原理以及...

    Android第三方开源框架ImageLoader的完美Demo

    `ImageLoader`就是这样一个被广泛使用的第三方开源框架,它为开发者提供了强大的图片异步加载和缓存功能。本Demo详细展示了如何在Android项目中集成和使用`ImageLoader`,以实现高效、流畅的图片显示。 `...

    安卓图片加载缓存相关-引用开源框架通过AsyncHttpClient实现网络图片查看器.rar

    这个压缩包“安卓图片加载缓存相关-引用开源框架通过AsyncHttpClient实现网络图片查看器.rar”显然是一个关于如何利用开源库AsyncHttpClient来实现图片加载和缓存的教学资源。AsyncHttpClient是一个轻量级的HTTP...

    java缓存_源代码

    在给定的资源中,我们可以看到四个项目源代码,分别与缓存相关的技术有关,可能是为了演示或实现不同的缓存策略。此外,还有一个"java缓存_other"目录,其中可能包含与这些缓存系统相关的文档,如设计文档、使用手册...

    图片缓存框架使用

    9. **持续更新和维护**:由于开源框架的不断发展,开发者应该定期更新所使用的缓存库,以获取最新的性能改进和修复。 综上所述,正确使用图片缓存框架是提升Android应用体验的关键。选择适合的框架,理解其工作原理...

    SSM(详细注释代码清晰)开源框架

    04、框架集成了代码表缓存的功能,查询时不需要再关联到字典表;通过拦截器进行权限检查、日志输出等操作; 05、框架实现了文件上传共通、电子文档导出、校验、全局异常处理、分页等共通,具体参见相关画面的相关...

    基于 springboot、ant-design-vue 的开源框架+源代码+文档说明

    以Spring Framework为核心容器,Spring data Jpa(Hibernate实现)为数据访问层, Apache Shiro为权限框架,Redis对常用数据进行缓存,前端使用Vue全家桶,前后端分离、JWT鉴权的开源框架。 角色的功能权限控制方式为...

    c#Aries开源框架系统源码

    框架自带分布式缓存(MemCache、Redis),轻松实现分布式缓存。 1:Aries.DataBase 初始:数据表脚本、数据脚本、数据库设计文档。 2:Aries.DevFramework 框架源代码。 3:Aries.Document API文档或帮助类文档...

    thinkphp php开源框架

    借鉴了国外很多优秀的框架和模式,使用面向对象的开发结构和MVC模式,融合了Struts的Action思想和JSP的TagLib(标签库)、RoR的ORM映射和ActiveRecord模式,封装了CURD和一些常用操作,单一入口模式等,在模版引擎、...

    PHP实例开发源码-CK PHP开源框架.zip

    【PHP实例开发源码-CK PHP开源框架.zip】是一个包含PHP编程实践的资源包,它基于CK PHP开源框架。这个框架是PHP开发者用于快速构建Web应用程序的工具,它提供了丰富的功能和灵活的架构,旨在提高开发效率并简化项目...

    大型网站架构演变和知识体系

    架构演变第二步:增加页面缓存 架构演变第三步:增加页面片段缓存 架构演变第四步:数据缓存 架构演变第五步: 增加webserver 架构演变第六步:分库,数据库集群 架构演变第七步:分表、DAL和分布式缓存 架构演变第...

Global site tag (gtag.js) - Google Analytics