缓存在很多程序里,读取数据的频率比写入要高得多。比如RoadRantz,访问站点来查看帖子的人比张贴帖子的人要多。虽然帖子列表会随着时间不断增长,但其增长速度比不上被查看的速度。 更进一步说,RoadRantz所展示的数据对于实时性要求并不高。如果用户在访问站点时看到了稍微过时一点的帖子列表,并不会产生太多负面影响,他们会稍后再返回站点来查看更新的帖子列表,这样做并不会有太大问题。 尽管如此,DAO每次收到关于帖子列表的请求时,都会访问数据库来获得最新的数据(经常会得到与上次请求一样的数据)。 数据库操作通常都是程序性能的最大瓶颈。对于负载很大的程序来说,针对高度优化的数据源进行的最简单查询都可能会产生性能问题。 均衡考虑数据变化的频率以及查询数据库所付出的性能代价,总是从数据库获取最新数据似乎并不明智,而对频繁访问(但不频繁更新)的数据进行缓存则显得更加合理。 从表面上看,缓存似乎相当简单:在获取一些信息之后,把它保存到本地(和更便于访问的)位置,从而便于下次需要时使用。但是,手工实现缓存是很麻烦的。以HibernateRantDao的getRantsForDay()方法为例:
public List<Rant> getRantsForDay(Date day) { return getHibernateTemplate().find("from " + RANT +" where postedDate = ?", day); } 这个方法就非常适合缓存。我们不可能让时间倒转,到过去的某一天来添加帖子。只要被查询的不是今天,对其他任意一天进行查询而返回的帖子列表都是一样的,也就没有必要总是对数据库进行操作来返回过去某一天的帖子列表。我们只需查询数据库一次,然后就可以记住结果,以备下次查询时使用。 下面我们来修改getRantsForDay(),使用某种自制形式的缓存:
这个版本的getRantsForDay()很不好用。这个方法的实际作用是查询指定日期的帖子,但其中大量代码都被用于处理缓存了。而且,它还没有直接处理缓存的一些复杂情况,比如缓存过期、刷新或溢出。
如图5.13所示,Spring Modules对于缓存的支持涉及到一个代理,它拦截对Spring管理的Bean的一个或多个方法的调用。当一个被代理的方法被调用时,Spring Modules Cache首先查阅一个缓存来判断这个方法是否已经被使用同样参数调用过,如果是,它会返回缓存里的值,实际的方法并不会被调用;否则,实际方法会被调用,其返回值会被保存到缓存里,以备方法下一次被调用时使用。 在这一小节里,我们将使用Spring Modules Cache为RoadRantz的DAO层添加缓存功能,这样会让程序具有更好的性能,让繁忙的数据库轻松一些。 5.7.1 配置缓存方案虽然Spring Modules会提供一个代理来拦截方法并把结果保存到缓存,它并没有提供一个实际的缓存解决方案,而是要依赖于第三方的缓存方案。可以使用的方案有多个,包括: n EHCache n GigaSpaces n JBoss Cache n JCS n OpenSymphony的OSCache n Tangosol的Coherence 我们为RoadRantz程序选择EHCache,主要是因为我以前使用它的经验及能够从www.ibibio.org的Maven仓库轻易获得。无论使用哪个缓存方案,对于Spring Modules Cache的配置基本上都是一样的。 首先要做的是新建一个Spring配置文件来声明缓存。虽然可以把Spring Modules Cache配置放到RoadRantz程序加载的任意一个Spring上下文配置文件里,但最好还是把它们分开,所以我们要创建roadrantz-cache.xml来保存缓存的配置。 与Spring上下文配置文件一样,roadrantz-cache.xml也以<beans>元素为根。但为了利用Spring Modules对EHCache的支持,我们要让<beans>元素能够识别ehcache命名空间: <beans xmlns="http://www./schema/beans" 我们为RoadRantz程序选择的是EHCache,如果想使用其他缓存方案,需要把Spring Modules命名究竟和规划声明修改为相应的内容。表5.6列出了每个命名空间及其URI和规划URI。
表5.6 Spring Modules所支持的缓存方案的命名空间及规划
无论选择哪种缓存,都可以使用一些Spring配置元素在Spring里对缓存进行配置。表5.7列出了这些元素。
表5.7 Spring Modules的配置元素
在使用EHCache作为缓存方案时,需要告诉Spring到哪里寻找EHCache配置文件[5],这正是<ehcache:config>元素的用途所在: <ehcache:config configLocation="classpath:ehcache.xml" /> 在此对configLocation属性的设置告诉Spring从程序类路径的根位置加载EHCache的配置。 配置EHCache我们已经配置了ehcache.xml文件,如程序清单5.12所示。 程序清单5.12 在ehcache.xml里配置EHCache。 在这段代码里,我们配置了两个缓存让EHCache进行管理。<defulatCache>元素是必须有的,描述了在没有找到其他缓存情况下所使用的缓存。<cache>元素定义了另一个缓存,可以在ehcache.xml里出现0次或多次(每次针对定义的一个缓存)。在此,我们只定义了rantzCache作为惟一的非默认缓存。 <defaultCache>和<cache>里指定的属性描述了缓存的行为。表5.8列出在配置EHCache缓存时可以使用的属性。
表5.8 EHCache的缓存配置属性
对于RoadRantz程序,我们配置了一个默认缓存(这是EHCache要求的),还配置了一个名为rantzCache的缓存作为主缓存。两个缓存都设置为最多可以容纳500个元素(不过期),访问频率最低的元素会被踢出,不允许磁盘溢出[6]。 在Spring程序上下文里配置的EHCache之后,就可以声明哪个Bean和方法应该对结果进行缓存。首先,我们来声明一个代理来缓存RoadRantz DAO层里方法的返回值。 5.7.2 缓存的代理Bean我们已经知道HibernateRantDao里的getRantsForDay()方法很适合进行缓存。再回到Spring上下文定义,我们要使用<ehcache:proxy>元素把一个代理包裹到HibernateRantDao,从而缓存从getRantsForDay()返回的全部内容: <ehcache:proxy id="rantDao" refId="rantDaoTarget"> <ehcache:caching>元素声明哪个方法要被拦截、其返回值要保存到哪个缓存。本例中,methodName被设置为getRantsForDay(),要使用的缓存是rantzCache。 我们可以根据需要在<ehcache:proxy>里声明多个<ehcache:cacing>来描述Bean方法的缓存。我们可以让一个<ehcache:caching>用于所有被缓存的方法,也可以使用通配符为一个<ehcache:caching>元素指定多个方法。比如下面的<ehcache:caching>元素会代理缓存全部名称由get开头的方法: <ehcache:caching methodName="get*" cacheName="rantzCache" /> 把数据放到缓存里只完成了一半的工作。在经过一段时间之后,缓存里一定会包含大量数据,其中很多已经没有意义了。最后,这些数据应该被清出缓存,数据缓存周期重新开始。下面我们来看一看如何在方法调用时刷新缓存。 刷新缓存<ehcache:caching>元素声明的是要向缓存中添加数据的方法,而<ehcache:flushing>元素声明了会清空缓存的方法。举例来说,假设我们想在saveRant()方法被调用时清空rantzCache缓存,那么就应该使用如下的<ehcache:flushing>元素: <ehcache:flushing methodName="saveRant" cacheName="rantzCache" /> 在默认情况下,cacheName属性里指定的缓存会在methodName被调用之后清空,但利用when属性可以指定清空的时机: <ehcache:flushing methodName="saveRant" cacheName="rantzCache" when="before" /> 把when属性设置为before可以让缓存在saveRant()被调用之前清空。 声明一个被代理的内部Bean注意<ehcache:proxy>的id和refId属性。由<ehcache:proxy>生成的代理的id是rantDao,然而这是HibernateRantDao Bean的id,因此,我们需要把这个真正的Bean重命名为rantDaoTarget(由refId属性指定)。(这与传统Spring AOP代理及其目标的命名方式是一样的,详情请见4.2.3小节。) 如果觉得id/refId组合有些奇怪,我们还可以把目标Bean声明为<ehcache:proxy>的内部Bean。举例来说,下面就是把HibernateRantDao配置为一个内部Bean的<ehcache:proxy>: <ehcache:proxy id="rantDao"> 即使使用了内部Bean,我们仍然需要为每个要代理的Bean声明一个<ehcache:proxy>元素,为方法声明一个或多个<ehcache:caching>元素。对于简单程序来说,这样做不会有什么问题,但随着代理缓存Bean和方法的数量不断增加,这将意味着Spring配置里越来越多的XML。 如果对内部Bean的方法仍然感到不快,或是需要代理多个要缓存的Bean,我们可以考虑使用Spring Modules对注解声明缓存的支持。接下来,让我们忘记<ehcache:proxy>,看一看Spring Modules如何支持注解驱动的缓存。 5.7.3 注解驱动的缓存除了前面介绍的基于XML的缓存配置,Spring Modules还支持使用代码级元数据声明缓存。这种支持有两种形式: Java 5注解:如果目标环境是Java 5平台,这就是很理想的解决方案。 Jakarta公共属性:如果目标环境是Java 5以前的平台,就应该选择它。 对于RoadRantz程序来说,其目标环境是Java 5,所以我们要使用Java 5注解来声明DAO层的缓存。对于缓存,Spring Modules提供了两个注解: @Cacheable:声明一个方法的返回值应该被缓存。 @CacheFlush:声明一个方法是清空缓存的触发器。 利用@Cacheable注解,我们可以像下面这样把getRantsForDay()声明为要被缓存的: @Cacheable(modelId="rantzCacheModel") modelId属性指定用于缓存方法返回值的模型,稍后我们介绍说明如何定义缓存模型,现在先来看一看如何使用@CacheFlush来指定saveRant()被调用时的缓存清空操作: @CacheFlush(modelId="rantzFlushModel") modelId属性指定的刷新模型会在saveRant()方法被调用时被清空。 既然说到缓存模型和刷新模型,那么它们是从何而来的呢?<ehcache:annotateions>元素被用于启动Spring Modules对注解的支持,我们会在roadrantzcache.xml文件里像下面这样配置它: <ehcache:annotations> 在<ehcache:annotateions>元素里,必须配置至少一个<ehcache:caching>元素,它就定义了一个缓存模型。简单来说,缓存模型基本上就是对ehcache.xml里配置的一个缓存的引用。本例中,我们把rantzCacheModel与名为rantzCache的缓存关联起来,这样一来,任何modelId是rantzCacheModel的@Cacheable都会使用名为rantzCache的缓存。 刷新模型与缓存模型相当类似,只是它引用的是要被刷新的缓存。下面使用<ehcache:flushing>元素创建一个名为rantzFlushModel的刷新模型: <ehcache:annotations> 设置缓存模型与刷新模型的不同之处在于,刷新模型不仅决定要清空哪个缓存,还决定了何时清空。在默认情况下,缓存是在@CacheFlush注解的方法被调用之后清空的,但我们可以通过指定<ehcache:flushing>的when属性来改变: <ehcache:annotations> 把when属性设置为before之后,缓存就会@CacheFlush注解的方法被调用之前清空。 |
|