第23章. 缓存
在几乎所有的企业应用程序中,数据库是主要的瓶颈,并是在运行环境中伸缩性最差的一层。来自PHP/Ruby 环境的人们试图告诉你所谓的“无共享(shared nothing)”体系结构的伸缩性良好。虽然这表面上是真的,但我知道大部分引人注意的多用户应用程序,并没有在群集的不同结点之间实现资源的无共享。这些愚蠢的人真以为它是一个“除了数据库以外无共享(share nothing except for the database)”的体系结构。当然,共享数据库是伸缩一个多用户应用系统的主要问题——因此而声称这种体系结构是高伸缩性是可笑的,并告诉你有关的很多应用,这些人浪费大量的时间在这上面。
我们能做的减少数据库共享的几乎任何东西都是值得去做的。
这需要缓存。嗯,不只是一个缓存。一个设计良好的Seam应用程序会展示出一个丰富的、多层的缓存策略,影响到应用程序的每一层:
- 数据库,当然,有它自己的缓存。这超级重要,但不能象应用层的缓存一样伸缩。
- 你的ORM解决方案 (Hibernate,或者其它的JPA实现)具有一个来自数据库的数据的二级缓存。这是一种超强的能力,但它常常被滥用。在一个群集环境中,保持缓存中的数据在整个集群上的事务一致性,使用数据库,是相当昂贵的。对多数用户之间共享和很少更新的数据它很有意义。在传统的无状态体系结构中,人们常常试图对会话状态使用二级缓存。在Seam中这始终是糟糕和特别错误的。
- Seam对话(conversation)上下文是一个会话状态的缓存。你装入对话(conversation)上下文的组件可以维持和缓存与当前用户交互的状态。
- 特别是,Seam管理的持久上下文(或继承EJB容器管理的持久化上下文,关联到一个对话作用域(conversation-scoped)有状态会话bean)作为一个已被读入当前对话(conversation)的数据的缓存。这种缓存往往有相当高的命中率!在一个群集环境,Seam优化了Seam管理的持久化上下文的复制,并且对数据库事务一致性没有要求(乐观锁足够了)。因此你不必太担心这种缓存的性能影响,除非你读入了成千上万个对象到一个单独的持久化上下文中。
- 在Seam的应用(application)上下文中,应用程序可以缓存非事务状态。当然,保持在应用上下文中的状态对群集中的其它结点是不可见的。
- 应用程序使用Seam的cacheProvider 组件可以缓存事务状态,其可集成JBossCache、JBoss POJO缓存或EHCache到Seam环境。这种状态对其它节点会是可见的,如果你的缓存支持运行在群集模式。
- 最后, Seam让你的缓存渲染JSF页面的片段。不象 ORM二级缓存,在数据改变时,这种缓存不能自动失效,因此,你需要写应用代码来执行明确失效,或设置适当的到期政策。
关于二级缓存的更多信息,你需要参考你的ORM 解决方案,因为这是极其复杂的话题。在本章我们将直接讨论缓存的使用,通过cacheProvider 组件, 或作为页面片段缓存,通过 <s:cache>控件。
23.1. 在Seam中使用缓存
内建的cacheProvider组件管理了下面的一个实例:
JBoss Cache 1.x (适用于JBoss 4.2.x和其它容器)
org.jboss.cache.TreeCache
JBoss Cache 2.x (适用于JBoss 5.x和其它容器)
org.jboss.cache.Cache
JBoss POJO Cache 1.x (适用于JBoss 4.2.x和其它容器)
org.jboss.cache.aop.PojoCache
EHCache (适用于所有容器)
net.sf.ehcache.CacheManager
你可以在缓存中放心地设置任何不变的Java对象,它会被存储在缓存中并可以在整个群集上复制(假定支持复制并被激活)。如果你想保持可变对象在缓存中,读取下属缓存项目文档的文档,以便发现如何通知缓存其发生了变化。
为使用 cacheProvider, 你需要包括缓存实现的jar包文件在你的项目中:
JBoss Cache 1.x
· jboss-cache.jar - JBoss Cache 1.4.1
· jgroups.jar - JGroups 2.4.1
JBoss Cache 2.x
· jboss-cache.jar - JBoss Cache 2.2.0
· jgroups.jar - JGroups 2.6.2
JBoss POJO Cache 1.x
· jboss-cache.jar - JBoss Cache 1.4.1
· jgroups.jar - JGroups 2.4.1
· jboss-aop.jar - JBoss AOP 1.5.0
EHCache
· ehcache.jar - EHCache 1.2.3
技巧
如果你在JBoss 应用服务器以外的容器中使用JBoss Cache , 为更多相关内容请看JBoss Cache wiki 页面。
对一个Seam的EAR部署, 我们推荐将缓存的jar包和配置文件直接打包在EAR中。
对JBossCache,你也需要提供一个配置文件。放带有适当缓存配置的 treecache.xml文件到类路径(例如ejb jar或WEB-INF/classes)。JBossCache有许多恐怖和混乱的配置设置,所以在这里不讨论它们。为更多信息请参考JBossCache文档在examples/blog/resources/treecache.xml中你可以发现一个treecache.xml的例子。
没有配置文件,EHCache会运行使用它的默认配置。
改变在使用的配置文件,在components.xml配置你的缓存:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:cache="http://jboss.com/products/seam/cache">
<cache:jboss-cache-provider configuration="META-INF/cache/treecache.xml" />
</components>
现在,你可以注入缓存到任何Seam组件:
@Name("chatroomUsers")
@Scope(ScopeType.STATELESS)
public class ChatroomUsers
{
@In CacheProvider cacheProvider;
@Unwrap
public Set<String> getUsers() throws CacheException {
Set<String> userList = (Set<String>) cacheProvider.get("chatroom", "userList");
if (userList==null) {
userList = new HashSet<String>();
cacheProvider.put("chatroom", "userList", userList);
}
return userList;
}
}
如果你想在你的应用程序中使用多个缓存配置,使用 components.xml配置多个缓存提供商:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:cache="http://jboss.com/products/seam/cache">
<cache:jboss-cache-provider name="myCache" configuration="myown/cache.xml"/>
<cache:jboss-cache-provider name="myOtherCache" configuration="myother/cache.xml"/>
</components>
23.2. 页面片段缓存
在Seam 中最有趣的缓存使用是<s:cache>标签, 在JSF 中的页面片段缓存问题的Seam解决方案。<s:cache> 在内部使用了pojoCache,所以在你使用它之前,你需要遵循上面所列的步骤。(设置jar包到EAR内,很吃力地通过 这些恐怖的配置选项,等等)
<s:cache>被用来缓存某些很少变化的渲染内容。 例如,我们博客的显示最新博客条目的欢迎页面:
<s:cache key="recentEntries-#{blog.id}" region="welcomePageFragments">
<h:dataTable value="#{blog.recentEntries}" var="blogEntry">
<h:column>
<h3>#{blogEntry.title}</h3>
<div>
<s:formattedText value="#{blogEntry.body}"/>
</div>
</h:column>
</h:dataTable>
</s:cache>
key让你在每个页面片段有多个缓存的版本。在这个情况下,每一个blog有一个缓存。Region决定了所有版本会被存入的缓存或区域节点 。不同的节点可能有不同的到期策略。(你使用前述恐怖的配置选项设置这些东西)
当然,使用<s:cache>的大问题是什么时候基础数据变化了知道得太迟钝(例如,当博客粘贴一个新的条目时)。因此你需要手工驱逐(evict)缓存片段:
public void post() {
...
entityManager.persist(blogEntry);
cacheProvider.remove("welcomePageFragments", "recentEntries-" + blog.getId() );
}
另外,如果立即显示变化给用户不是重要的,在缓存节点上你可以设置一个很短到期时间。