数据库成为了大多数企业应用的主要瓶颈,也成为了运行环境中最不具伸缩性的层。PHP/Ruby的用户会说什么都不共享(share nothing)的架构照样具有很好的伸缩性。从表面上看也许是对的,可惜我不知道是否存在这样的多用户应用,其实现是能够在集群的不同结点间不共享资源。这些傻瓜真正想的是“除了数据库以外什么都不共享(Share nothing except for the database)”的架构。当然,共享数据库是多用户应用伸缩性的主要问题——因此声称这样的架构具有高伸缩性是荒谬的,你可要知道它们花费了这些人的大部分时间。
通常,几乎所有通过共享数据库做的事情并不值得这样去做。
这就是缓存(Cache)产生的原因。嗯,当然并不只是一个缓存。一个设计良好的Seam应用将具有丰富的多层缓存策略,这也影响着应用的每一层:
当然,数据库有它自己的缓存,这是超级重要的,但是它不能像应用层的缓存一样具有伸缩性。
对从数据库提取出的数据,你的ORM解决方案(Hibernate,或者别的JPA实现)具有两级缓存。这是一种很强大的能力,但是经常被误用。在一个集群环境里,保持缓存中的数据在整个集群中具有事务一致性,并且和数据库一致,其代价是相当昂贵的。这对于共享在多个用户间,且很少被更新的数据最有意义。在传统的无状态架构里,人们经常使用二级缓存来保存会话状态。这种做法总是糟糕的,在Seam中更是大错特错的。
Seam会话上下文是会话状态的缓存。存储于会话上下文中的组件可以保持并缓存与当前用户交互相关的状态。
特别的,Seam管理的持久化上下文(或者一个扩展受管EJB容器持久化上下文,它与会话范围的无状态会话Bean相关)成为了当前会话中数据的缓存。这种缓存趋向于拥有一个相当高的命中率!Seam优化了集群环境中受管Seam持久化上下文的复制,也不需要保证数据库事务的一致性(乐观锁已足够),因此你不必担心这种缓存的性能问题,除非你把成千上万个对象读取到一个单独的持久化上下文中。
应用可以在Seam应用上下文中缓存非事务性状态。相应的,保存在应用上下文中的状态不能被集群中其它结点访问。
应用通过Seam的 pojoCache 组件可以缓存事务性状态,这个组件把JBossCache集成到了Seam环境中。如果你在集群模式下运行了JBossCache,那么这个状态是可以被别的结点访问的。
最后,Seam让你能够缓存生成的JSF页面的部分内容(rendered fragments)。与ORM的二级缓存不一样的是,当数据发生变化时,这种缓存不能自动的失效,因此你需要写应用代码来使它显式的失效,或者设置适当的过期策略。
如要获得更多关于二级缓存的信息,你可以参考你的ORM解决方案的文档,因为这是个极为复杂的话题。在这节中我们会直接讨论通过 pojoCache 组件使用JBossCache,或者通过 <s:cache>控制充当页片段(page fragment)缓存。
内建的 pojoCache 组件用来管理 org.jboss.cache.aop.PojoCache 实例。你可以安全的把任何不可变的Java对象保存在缓存里,然后它将通过集群被复制(假设打开了复制)。如果你想在缓存里保存可变的Java对象,你需要运行JBossCache字节码预处理器来确保这些对象的变化能够自动的被探测到并被复制。
为了使用 pojoCache,你需要做的就是把JBossCache jar文件放在classpath中,并提供一个名为 treecache.xml 的文件,它应该有一个合适的cache配置。JBossCache有很多让人觉得恐怖和迷惑的配置,因此我们不在这里讨论它们。请参考JBossCache文档来获得更多信息。
在 examples/blog/resources/treecache.xml 里有一个 treecache.xml 的范例。
对Seam的EAR部署,我们推荐把JBossCache jar文件和配置文件直接放到EAR里。确保你在EAR的lib目录里放置了 jboss-cache.jar 和 jgroups.jar。
现在你可以把缓存注入到任何Seam组件:
@Name("chatroom") public class Chatroom { @In PojoCache pojoCache; public void join(String username) { try { Set<String> userList = (Set<String>) pojoCache.get("chatroom", "userList"); if (userList==null) { userList = new HashSet<String>(); pojoCache.put("chatroom", "userList", userList); } userList.put(username); } catch (CacheException ce) { throw new RuntimeException(ce); } } }
如果你想在应用中使用多个JBossCache配置,需要使用 components.xml:
<core:pojo-cache name="myCache" cfg-resource-name="myown/cache.xml"/>
使用JBossCache最有趣的是 <s:cache> 标签,Seam用它来解决JSF的页片段缓存问题。<s:cache> 内部使用了 pojoCache,因此你在使用它时需要遵循上面列出的步骤。(把jar文件放到EAR里,并进行令人恐怖的配置选项等。)
<s:cache>被用来缓存一些不太变化的内容。例如我们blog欢迎页面显示的近期blog:
<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 决定了所有版本都将保存在哪个JBossCache结点里。不同结点可能有不同的过期策略。(这是你设置上述令人恐怖的配置选项做的事情。)
当然,<s:cache>的一个重要问题是它无法知道数据的变化(例如当博客作者发布一个新实体)。因此你需要自己管理被缓存的片段。
public void post() { ... entityManager.persist(blogEntry); pojoCache.remove("welcomePageFragments", "recentEntries-" + blog.getId() ); }
另一种方法是,如果不必把数据的变化立刻反馈给用户,那么你可以在JbossCache结点上设置一个短的过期时间。