J2EE规范现在作为同时期企业项目的标准被广为接受。但是J2EE规范的一个重要部分即EJB持久性由于它的开发模型复杂并且实体bean的性能很差而长期受到批评。人们相信这样一个事实:如果实体bean(尤其是容器受控持久性实体bean,或者CMP)用于应用程序中,那么性能将受到影响。事实并非如此。
本文中我不打算解释EJB的复杂性。即将推出的EJB 3规范专门针对目标和开发模型,使得它更容易;该规范还提供依赖注入以及在实体bean容器之外的更容易的测试。相反,本文的目标在于提供BEA WebLogic Server 8.1和9.0中可用的高级选项的深度分析,使开发人员改善CMP bean的性能——在很多情况下可极大地改善。该主题很宽泛,不可能在一篇文章中一一涉及;因此,我只重点讨论CMP实体bean的并发以及长期缓存策略。我还简要说明了最新版本BEA WebLogic Server 9.0中的改进。
并发策略
J2EE开发人员知道EJB容器维护了一个实体bean缓存或者池,通常可在部署描述符中配置。令人惊奇的是,相当多的J2EE开发人员不知道这并不意味着一旦J2EE服务器从数据库中加载一个特定的bean实例,它就不再去数据库中寻找该实例,因为该实例已经保存在缓存池中了。相反,默认情况下J2EE服务器执行ejbLoad()在每次事务的开始从数据库中同步该实例的状态。基本上,CMP bean每运行一次(即使该bean在前一个事务中已经被加载),服务器就执行一次SQL select语句来刷新它。只有在一个事务中操作多个实体bean实例时,服务器才会缓存它们。
显然,在每次事务中都重新从数据库中加载状态会造成很大的性能影响!这个默认行为很容易理解:如果数据库被多个进程共享,并且每个进程都可以改变数据库中持久对象的状态,那么这将是最安全的方法。但是可以通过告诉J2EE服务器保留事务间实体bean的缓存实例,从而避免大部分时间里从数据库中刷新数据来略微改善这种情况。为了解决这个问题并生成一个最优的解决方案,首先我将讨论BEA WebLogic Server中可用的不同的并发策略。
对于EJB开发人员来说很重要的一点是要知道实体bean中可用的不同并发策略。令人惊奇的是,有的开发人员甚至不知道并发选项的存在。那么适用于实体bean的并发策略是什么呢?EJB容器是一个高度多线程的应用程序,同时响应来自多个客户端的请求,这些请求通常会访问同一资源,比如数据表中的一行。因此,EJB容器应该管理对实体bean实例的并发访问;更加技术性地讲,并发策略决定了容器如何以及何时将实体bean的每个实例与底层数据库同步。
目前WebLogic Server中有四种可用的并发策略:排他、数据库、乐观和只读。默认情况下,从7.0版本开始,WebLogic Server就使用的是数据库并发。上面四种策略按性能从低到高依次排列。我将讨论每种策略的优缺点。
排他性并发
排他性并发意味着容器最多为每个主要键值创建一个实体bean实例(比如,表中的一行映射到容器中的一个EJB实例)。对指定实例的访问是串行的,并且请求是按照顺序逐个执行的。这种策略有一些严重的问题。首先,性能由于多个客户端对bean的串行访问受到明显影响,并且您不能再考虑应用程序的伸缩性。其次,EJB的单个实例(以及容器持有的关联锁)对于一个JVM(一个服务器实例)来说是本地的,不能在集群中工作。该策略只是用于后向兼容(早期版本的WebLogic Server默认使用它),应该尽量不用。
数据库并发
数据库并发策略是目前WebLogic Server版本中的默认并发策略。它提供了数据持久性和性能间的折中考虑。原理很简单:WebLogic Server并不自己管理锁,而是为每个试图访问该bean的每个事务创建一个新的bean实例,并将并发控制和死锁检测委派给底层数据库。这就像多个客户端对单个数据库进行并行数据库操作;数据库的隔离水平和锁定策略将规定哪些更新、选择和插入会进行,按照何种顺序,以及哪些(如果有的话)会失败。直接好处是该策略在集群环境中的良好适用性——只要集群中的所有节点共享一个数据库,EJB容器就不需要为数据同步细节而烦恼。
该策略明显比排他性策略更具伸缩性,并且对于某些应用程序效果尤为出众,但是也无法摆脱一些严重的性能限制。即使这样,容器仍保持了一个实体bean实例池,并且这些实例不包含事务间的任何中间状态。这是实例池化而不是缓存数据。池化无状态实例的整体思想可能来自于早期的JVM实现,那时对象创建还是一项很昂贵的操作,并且从性能的角度来看缓存对象实例是有好处的。在现代的JVM中情况并非如此,因为大部分情况下对象的创建非常快,但是由于该行为是EJB规范中描述的,所有供应商都应支持它。然而,当使用数据库并发策略时,容器从缓存中取出“无状态的”bean实例,并且必须执行一条SQL选择操作以获得最新数据并填充实例字段。
这种方法可能还不错,因为我们不用担心“不新鲜的”bean实例(当数据库中的数据被从同一集群中的另一个节点或者从不同应用程序中更新时),但是性能也同样受到明显影响。您总是在每次事务的开始以一个额外的select操作结束,即使您只是打算更新bean中的数据而对之前的值并不感兴趣。因此,在主要或仅是执行更新或插入操作的应用程序中使用实体bean意义不大——容器可能花大量时间做不必要的选择操作,然后再抛弃数据。
排他性和数据库并发策略至少存在一个共同问题:更新丢失的可能性。可以想象两个客户端几乎同时更新映射到一个实体bean的表中的同一条记录。如果数据库中没有锁,先完成的更新操作的结果会被其次完成的更新所覆盖。这是否是可接受的结果取决于您的业务需求。更新丢失通常是不可接受或者不想要的;因此,应用程序需要某种机制来避免或检测更新丢失的情况,并且有机会恢复。当应用程序部署再多个节点上时使用排他性策略将不能控制更新丢失问题。但是如我之前所述,您不应再考虑该该策略。
数据库策略通过将并发控制委派给数据库,提供了进行读数据操作时在数据库中使用排他性锁的选择。这是通过将weblogic-cmp-jar.xml中的use-select-for-update元素设置为true(默认为false)来实现的。顾名思义,该动作告诉WebLogic Server在加载实体bean数据时使用“select for update”。生成的数据库锁一致存在,直到事务完成,因此其他事务不可能在第一个事务运行期间读取或更改数据。该项技术也许在“select for update”上组合了“no wait”选项,可能解决更新丢失问题以及任何可能的死锁——只不过代价很高。
该方法的一个缺点是性能会降低,因为一旦一个事务锁住一个实体实例,其他事务就不能访问同样的实体数据,即使它们需要的是只读访问。这基本上就是另一种排他性策略,唯一的区别是这次它可用于集群环境,因为所有的锁定都发生在(共享)数据库中而不是服务器实例上。第二个缺点是bean的行为在某种程度上依赖于底层数据库的锁策略实现。比如,有些数据库支持细粒度、行级的锁定,而有些则不然。在后一种情况中,对整个记录页的访问可能被作为单个实体加载操作的结果而被阻止。
乐观并发
乐观并发策略通过消除读取和更新操作之间数据库中的所有锁定解决了性能问题,同时还提供了检测更新丢失的机制。和数据库并发一样,乐观并发给每个事务一个自己的bean实例,但是它没有在事务进行过程中在EJB容器或数据库中包含任何锁。对于进行读锁定的数据库(非Oracle数据库),容器在一个单独的事务(该事务在数据读取操作一完成就立即进行)中读取bean数据,从而避免了读取锁,并且提供了更高的可伸缩性。
这种更新丢失检测机制并不新鲜,在Java创建之前就已经存在好长时间了。该模式的基本思想简单而强大:在更新时,首先进行检查,以确定表中的数据在被第一个进程读取之后是否被其他进程修改。实际中,这通常是通过在update语句的where子句中包含一个或多个额外的“版本”列来实现的。下面给出了一个简单的SQL的例子,其中version列是一个数值,在每次更新操作之后会递增:
-- at Entity load time"container executes select of bean fields,
-- plus 'version' field
select name, address, version from person where person_id = 1;
-- container remember value read from version column,
-- say it was 123
-- at update time, value of version which was read previously
-- added to the where clause and version incremented
update person set name='Joe', version = version + 1 where person_id = 1 and version = 123;
在进行更新时,容器通过执行以下代码能检测出数据库中实际上有多少行被更新:
...
PreparedStatement ps =
connection.prepareStatement('update person...');
int rowsUpdated = ps.executeUpdate();
if (rowsUpdatated != 1) {
throw new OptimisticConcurrencyException('Data was modified by other process');
}
如您所见,该方法可使服务器避免更新丢失问题,并且不需要在容器或数据库中进行长期的锁定。该策略尤其适用于数据库中读取次数远多于更新次数,因此更新碰撞的几率很小时。通常,对于大部分应用程序来说是这样的。
一个用数字表示的版本列不是在WebLogic Server中实现乐观并发策略的唯一途径。相反可以使用一个时间戳列。(注意,数据库应该允许您存放足够精确的时间戳值)。时间戳的使用给您带来了额外好处,那就是知道记录的最后一次更新是什么时候。有时候在使用遗留数据库模式时,会不愿意或不可能更改数据表以添加一个版本列。这种情况下,WebLogic Server可以检查表中事务期间被读取的所有列(实体bean中的所有字段),或者只检查被当前事务更新的列。乐观并发可用于遗留模式,无需对数据表进行任何修改,但是开销略微增加(意味着更复杂的SQL更新)。
只读并发
最后一个并发策略是read-only并发。如果数据库中的某些表包含很少或从未被更改过的数据,那么就很有必要将CMP bean声明为只读。服务器仍然为每个事务激活一个新的bean实例,所以请求是并行处理的;它不会每次都调用ejbLoad(),但是根据read-timeout-seconds参数的值周期性地调用。这为您的应用程序带来了显著的性能提升,因为SQL选择只进行一次(第一次访问实体bean时),然后就被缓存起来,在后面的事务中重用。
WebLogic Server 8.1 SP2之前版本中的一个特殊功能是,即使bean被部署为只读,开发人员仍然可以对该bean的实例进行create和remove操作。从8.1 SP2开始,这个功能在默认情况下已被禁用,但是如果您需要,可以通过在weblogic-cmp.jsr.xml中将allow-readonly-create-and-remove元素设置为true来打开它。
也有一种明确禁用只读实体bean的方法。该禁用操作强制在下一次事务开始时从数据库中刷新bean实例,即使读取时限还没过去。您可以将其看作冲洗实例缓存。可以使特定bean的一个特定实例,任何实例子集,或者给定bean的所有实例无效。要想调用invalidate()方法,您可能需要将bean的home或local home分别转换为CachingHome或CachingLocalHome。下面的代码说明了如何实现这一点:
Object o = context.lookup("MyBeanCMP");
// cast to CachinHome for remote interface or CachingLocalHome
// for local
CachingHome cachingHome = (CachingHome)o;
// invalidate particular instance by passing primary key value
cachingHome.invalidate(pk);
// invalidate any subset of bean instances by passing collections
// of primary keys
Collection pks = Array.asList(new Object[]{pk1, pk2, ...., pkN});
cachingHome.invalidate(pks);
// or invalidate all instances currently cached
cachingHome.invalidateAll();
当表中的数据准静态时(比如,如果它按照批处理过程每天更改一次)进行显式的无效操作很有用。这种情况下,您可以将相应的bean部署为只读,并且设置一个较大的读取超时值,然后,当批处理过程结束时,为这些实体bean调用invalidateAll()。
您可以通过在weblogic-ejb-jar.xml部署描述符中entity-cache一节中设置concurrency-strategy元素,为每个CMP bean指定一种并发策略。如果没有指定并发策略,WebLogic Server默认使用数据库并发。
性能改善策略
既然大家已经熟悉WebLogic Server中的各种并发策略,现在就让我演示一下如何应用这些知识应用来提高CMP bean的性能。CMP bean通常由于性能不出众而遭到批评。从某种程度上说确实如此,因为如上所述,对于按照默认并发策略设置部署的bean,WebLogic Server会在每次新事务的一开始从数据库中读取数据。换言之,bean实例状态并没有在各个事务之间缓存,并且数据库收到大量的选择请求。有人争辩说,这没问题,因为现代的数据库本身就有有效的缓存机制。因此,一旦第一次被选择,数据就可能留在数据库缓存中,等待后面被调用,需要很少或者不需要磁盘活动来进行数据库选择。虽然这样,但是我们不应该忘记,大多数情况下应用服务器和数据库之间的调用要在网络间传输,调用的延迟要比JVM内部的本地调用高几个数量级。另一个要关心的是,多个应用服务器实例可以共同访问同一个数据库(通常在集群配置中)。这种情况下,数据可能会充满应用服务器和数据库之间的甚至是最快速的网络链接。
简而言之,如果我们需要好的性能和伸缩性,那么我们可用的最有效的策略就是如何在可能的情况下在本地缓存数据,以避免代价高昂的对数据库的远程调用。从CMP bean的角度来说,这意味着我们需要一种机制在来自各个不同事务的调用之间保存(或缓存)bean的状态数据。当然,这会给我们带来性能收益,只要同一个bean实例在它生命周期中被调用一次以上的几率大于零。换言之,您的应用程序读数据的机会要多于写数据,并且存在在每次更新之间多次访问同一数据的可能。比如,如果您的应用程序只向数据库中写数据(OLTP),缓存那些数据根本不能提高性能。同样,如果您有特别大的表,并且要随机选择某些行,那么就存在这样的可能:缓存数据要存在足够长的时间,以便在下次需要时可用。幸运的是,仍然有很多类型的应用程序满足我们的缓存标准。您需要针对具体任务评估缓存的有效性。
观察一下CMP bean的可用并发策略,您会注意到,至少有两种方法可以实现长期缓存。第一种方法是尽可能使用只读bean。不幸的是,通常数据库中的数据不是完全静态的,并且/或者按照不可预知的时间间隔更新。如前所述,有一种机制可以显式地使任何只读CMP bean的缓存无效。这种方法虽然可行,但是却很不理想并且易发生错误,因为开发人员必须记得在每次更新操作之后调用invalidate()。幸运的是,WebLogic Server提供了方便的read-mostly模式实现,这在稍后将会详细讨论。更强大的方法是充分利用使用乐观并发策略部署的bean可用的事务间缓存机制。
使用read-mostly模式
WebLogic Server通过将一个只读CMP bean和一个读写CMP bean映射到同一数据中为您提供了一个实现read-mostly模式的机制。您的应用程序应该用只读bean来读取数据,用读写bean来修改数据。只读bean按照上述部署描述符中read-timeout-seconds元素所指定的间隔从数据库中加载数据。为了保证只读bean总是返回当前数据,应该在读写bean更改数据时使只读bean无效。您可以通过在weblogic-ejb-jar.xml中的entity-descriptor小节的invalidation-target元素中指定该bean,来配置服务器,使其自动让相应的只读bean无效。这只能用于CMP 2.0实体bean。虽然该模式提供了缓存方面的好处,但是也有严重的不足。当使用该模式时,您应用程序中的大量实体bean将被有效地加倍,对应用程序的启动时间和内存造成影响。同样,开发人员需要记住,只读和读写操作应该使用不同的bean,这经常会令人混淆。
值得一提的是,在旧版本的WebLogic Server中(没有对只读模式的通过invalidation-target的内在支持),仍可以使用它。回忆一下前面讲过的,根据EJB规范,如果EJB抛出一个RuntimeException或者它的子类物,容器就应该销毁bean实例。因此,可以在实体bean的只读版本上暴露这样的destroyMe()方法,并从读写bean的ejbStore()方法中调用它。这就是著名的sepukku模式。
在读/写CMP bean的事务间缓存
另一种更先进的长期缓存的方法是通过将weblogic-ejb-jar.xml中的cache-between-transactions元素设置为true来配置bean。这种情况下,只有客户端首先引用该bean或者事务被回滚时WebLogic Server才会调用ejbLoad()来从数据库中加载数据。
尽管从理论上说,您可以对除数据库并发之外的所有并发策略使用该方法,但是在实践中,只有对乐观并发使用该方法才有意义。当应用于只读并发时,该设置被忽略,因为bean数据已经被缓存;当应用于排他性并发时,只有EJB具有对底层数据库的排他性访问时才起作用,而这是极少出现的情况。此外,当具有排他性并发的bean被部署在集群中时,WebLogic Server自动禁用事务间的缓存,因为集群中的任何服务器都可能更新bean数据,并且没有用来在节点间同步或禁用缓存值的机制。这使得我们在进行长期缓存时只有一种可行的并发策略:乐观并发。
如前所述,对于利用乐观并发策略部署的bean,WebLogic Server有一种内在机制,用来通过verify-columns检测底层数据变更。虽然乐观并发本身只能为数据库并发带来少量性能改善,但可利用事务功能间的缓存提供更大的改善。如果在EJB缓存中bean实例已经可用的话,将cache-between-transactions设置为true将使WebLogic Server忽略对数据库的调用。对于某些类型的应用程序(其中同一对象在短期内被不同事务多次访问),这可能导致显著的性能改善(在某些环境下,最多百分之30到40)。自然地,既然我们使用的是乐观并发,您的应用程序必须做好在检测到并发冲突时处理OptimisticConcurrencyException的准备。当OptimisticConcurrencyException(RuntimeException的子类型)被抛出时,WebLogic Server 从缓存中丢弃一个EJB实例。注意,如果您将delay-updates-until-end-of-tx设置为true(默认),除非事务承诺否则就得不到乐观异常,并且如果使用的是容器受控事务这将在应用程序代码之外。
与read-mostly模式(不提供通知集群中其他节点其中一个节点的数据发生变更的机制)相比,当具有乐观并发的bean被更新时,一个通知将被广播给其他集群成员,并且缓存bean实例将被丢弃,以避免乐观冲突。由于WebLogic Server不广播数据变更本身,而是只广播某些种类的bean标识符,这种跨集群的缓存无效措施在提高性能和网络带宽利用率方面很有效。WebLogic Server自动完成这种缓存无效工作,bean开发人员不需要再做其他配置。当对同一个bean发出下一个请求时,新鲜的数据将被从数据库中加载。
If the data in the database is updated by processes external to the server (or if you're using direct JDBC access from the server to modify the same tables as mapped to CMPs with long-term caching), then these processes should honor the contract on version columns.换言之,如果实体bean被配置为使用数值版本列,那么外部进程应该在行数据更新时增加该值;如果使用了一个时间戳列,那么这个值应该被更新为当前时间戳。如果不这样做会导致WebLogic Server覆盖数据,因为它的乐观检测机制不会触发异常,如果版本数据没有被更改的话。如果不可能通过修改外部进程来更新版本列,可用数据库触发器来实现同样效果。如果不允许修改数据库模式,可对WebLogic Server进行配置,使其检查事务期间被读取的所有列或者只读取更新过的列(通过分别将verify-columns元素设置为Read或Modified来实现)。注意,这种情况下,可能存在性能问题,因为生成的更新SQL语句更复杂。我建议进行测试,以确定这会对您具体环境中的更新造成多大影响。
在事务间进行缓存提供了比上面讨论过的read-mostly模式更好的缓存数据模型。首先,并没有增加复杂度,比如部署同一bean的两个版本。另外,对启动时间,以及当集群间bean发生变更时的自动缓存禁用等也没有造成影响。直到最近,WebLogic Server中与事务bean间缓存相关的一项特性还被忽视。没有公开的机制来有计划地使bean缓存无效。如果您对服务器上数据库进行排他性访问,这没有什么大问题,但是在很多项目中,很少有这种情况,并且为了清空缓存,必须重启实例;同样,这也并不总是可能的。
让我们看一下如果一个bean用乐观并发部署并且当数据库中的数据被外部进程更新时被在事务间缓存,这时会发生什么。如果一个外部进程更新当前被容器缓存的记录,然后应用程序通过CMP更新同样的列,那么会有两种可能的结果:如果外部进程在更新verify-columns时不遵守协定,那么就会出现更新丢失的情况(来自CMP的更新覆盖外部进程对记录进行的修改)。另一方面,如果外部进程更新了版本列,或者bean被配置,以便用Read/Modified列进行乐观控制,您就可能有一个OptimisticConcurrencyException。
在WebLogic Server中,OptimisticConcurrencyException是RuntimeException的一个子类,并且如果应用程序没有捕获它,实体实例(以及调用同一事务中该实体的所有会话bean)就被丢弃,事务就被回滚;下一次,WebLogic Server会从数据库中重新加载数据,并且事务会成功完成。尽管缺乏“美感”,但这种方法对于使用队列(MOM)的应用程序来说还是很有效的;在事务回滚时,该消息将保留在队列中,接着下一个重新交付尝试(如果有的话)会成功。值得再次一提的是,除非您的应用程序使用bean受控事务,否则您将捕获不到OptimisticConcurrencyException,除非将delay-updates-until-end-of-tx设为false(非默认值)。利用默认设置,WebLogic Server不能在数据库中执行实际的DML操作,并且操作会以RollbackException(它内部就是提到过的真正的OptimisticConcurrencyException)异常而失败。
下面基于Cactus的测试验证了该行为。假定一个Person CMP bean是利用乐观并发部署的,并且cache-between-transaction被设置为true:
...
public class OptimisticLockingTest extends ServletTestCase {
private UserTransaction tx;
protected void setUp() throws Exception {
super.setUp();
Context context = new InitialContext();
this.tx = (UserTransaction)context
.lookup("javax/transaction/UserTransaction");
}
public void testCacheBetweenTransactions() throws Exception {
PersonLocalHome localHome = (PersonLocalHome)Locator
.getLocalHome(PersonLocalHome.JNDI_NAME);
// create record via CMP in first transaction
this.tx.begin();
PersonLocal local = localHome.create();
local.setName("John");
Long pk = local.getPrimaryKey();
this.tx.commit();
// update some field(s) via direct JDBC call in another
// transaction. Assume that updatePersonNameViaJdbc()
// method will update version column as well
String newName = "Paul";
this.tx.begin();
updatePersonNameViaJdbc(pk, newName);
this.tx.commit();
// find CMP again and try to update name in yet
// another transaction
this.tx.begin();
local = localHome.findByPrimaryKey(pk);
// code doesn't see changes made directly
// because bean instance was cached between transactions
assertFalse(newName.equals(local.getName());
try {
// this should throw optimistic concurrency
// exception (on commit)
local.setName("George");
this.tx.commit();
fail("expected OptimisticConcurrencyException not thrown");
}
catch (RollbackException expected) {
// unfortunately there seems to be no better way to
// assert that underlying exception was
// an OptimisticConcurrencyException
assertTrue("Unexpected exception type: "+expected,
expected.getMessage()
.indexOf("Optimistic concurrency violation") > 0);
}
}
}
...
我希望您同意对长期缓存的失效进行控制是有益的。随着它的出现,甚至出现了WebLogic 7.0和8.1的解决方案。与可用于为只读bean清空缓存的CachingHome/CachingLocalHome接口类似,一个EntityEJBHome和一个EntityEJBLocalHome,再加上同系列的invalidate()方法,使应用程序能让特定实体bean的所有缓存或者一部分缓存无效。WebLogic Server中的任何CMP本地接口都可转换为EntityEJBLocalHome。利用前面的例子,我们可以在updatePersonNameViaJdbc()方法调用后插入下面的代码:
...
// flush cache
assertTrue("PersonLocalHome not instance of EntityEJBLocalHome: "+ localHome, localHome instanceof EntityEJBLocalHome);
EntityEJBLocalHome entityEJBLocalHome = (EntityEJBLocalHome)localHome;
entityEJBLocalHome.invalidate(pk);
...
现在当下一次调用findByPrimaryKey()时,bean实例将被从数据库中重新加载,并且所有一切变得更好。除了invalidate()方法,还有invalidateAll()和invalidate(Collection)方法。
WebLogic Server 9.0中的改进
在WebLogic Server 9.0中,对使用乐观并发的缓存bean进行的显式缓存禁用被归档,并且与只读bean一致。(比如,bean home或远程主接口可被缓存到上面调用的CachingHome或CachingLocalHome和invalidate()方法中)。此外,read-timeout-seconds参数适用于用乐观并发部署的bean。开发人员还对集群中的bean实例无效化有更多的控制。默认情况下,当在一个集群中部署具有乐观并发策略的bean,并且该集群的一个成员更新该bean时,WebLogic Server会试图使该集群的所有节点中的bean的所有副本无效。该无效化使您避免了乐观并发故障,但是会影响性能,因为它是一项资源密集型操作。可通过在weblogic-cmp-jar.xml中将cluster-invalidation-disabled设置为true来防止EJB容器使集群中的bean副本无效。
为实体Bean选择最佳缓存大小
既然您理解了事务间的缓存是如何工作的,下面就让我们讨论一下选择最佳缓存大小方面的重要话题。默认情况下,每个实体bean都定义有一个大小为1000的缓存。缓存大小由weblogic-ejb-jar.xml部署描述符中的max-beans-in-cache元素控制。我发现该名称有些令人误解,因为根据并发策略的不同,WebLogic Server保存无状态的实体bean实例池(采用数据库并发策略和排他性并发策略,且事务间缓存被禁止的情况下)或者(只读、乐观或排他性并发策略,且事务间缓存启用的情况下)保存具有保留字段值的bean的真正缓存,从而无需从数据库中重新加载bean的状态就可使用。后一种情况更有趣。有人可能会想,更改缓存大小只影响具有实体bean的操作的性能;缓存越大,在缓存中发现需要的特定实体bean实例的机会就越大。基本上是这样的,但是如我下面会讲到的那样,另一个重要因素会影响缓存大小的选择。
多版本化和事务的因素
确定实体bean缓存大小的推动因素之一(可能不太明显)是当事务使用实体bean时,它们在事务的执行期间被实际加载和“固定”在实体缓存中,即使调用者不在修改实体bean实例而仅是从中读取值。比如,想象一下,一个会话或MDB bean中的代码在一个实体bean “Person”上执行一个finder方法,然后在返回的集合上迭代。
...
Collection persons = personLocalHome.findSomething();
for (Iterator iter = persons.iterator(); iter.hasNext();) {
PersonLocal person = (PersonLocal)iter.next();
Long id = person.getId();
// do something: log, send email, etc
...
}
...
如果findSomething()方法返回比max-beans-in-cache中规定的值更多的对象,您的应用程序将在迭代器得到N+1个对象时(N为当前实体缓存大小)获得一个令人不愉快的(并且很可能是不想要的)CacheFullException。这可能看上去很重要,因为一般大家都认为finder不应返回很大的集合。但是不要忘记默认情况下WebLogic实体缓存是多版本的,这意味着如果多个事务请求同一个实体bean实例,那么会在缓存中创建多个版本(每个事务一个);从唯一对象的角度来看这可能极大地降低了缓存容量。
由于有多个事务同时运行在一个容器上很正常,所以可以想象如果上面的代码被从一个会话或MDB bean中调用,其中该bean是利用一个较高的max-beans-in-free-pool参数值(默认1000)部署的,并且同时有50个客户端请求。这使得每个事务在实体缓存中只有1000/50 = 20个可用的槽,如果一个finder返回的对象超过20个,有的事务就会失败。
在设计具有大量实体bean的操作时要时刻牢记这一点。开发人员通常使用小型数据库这一事实使情况变得更糟,并且该问题可能并不表现出来,直到代码部署到生产规模的数据库中时。作为保护措施,我建议在开发过程中不要使用默认的缓存大小,而是将其较低值(10-100),这样缓存相关的问题就能在开发早期发现并解决。
如您所见,为实体缓存选择正确的大小非常重要,并且不只是从性能的角度来看。如果缓存过大,您的应用程序将消耗很多不必要的内存,但是如果您走到另一个极端,配置过小的缓存空间,会有收到CacheFullException的风险。那么该如何为所有的实体bean选择最佳的缓存大小呢?
如果您没有明确为实体bean指定缓存大小,WebLogic Server将使用默认大小1000。这对于预先知道实例数不会太多的某些bean来说足够了——比如,如果一个bean表示数据库中的一个查询表,比如“country”或“state”,其中bean实例的上限是已知的。这种情况下,不指定缓存大小而让服务器使用默认值是完全可接受的,因为如果缓存没有被充分利用不会对内存造成影响。顺便指出,为不变化或不频繁变化的bean使用只读并发策略是一个不错的注意;这不但消除了不必要的数据库调用,还限制了与该bean的实体缓存中的实例具有同样PK的实例数(多版本是不是必要的),从而节省了内存,提高了性能。
对于其中可同时访问的最大实例数未知或不能可靠地估计出来的bean,情况略微复杂些。您需要分析和估计从finder方法返回以及从一个事务内访问的最大bean数,然后乘以可同时发生的最大事务数(这通常受您应用程序入口点的最大实例数限制——会话bean和/或MDB)。这能粗略地估计出特定实体bean所需的最小缓存容量。
应用程序级缓存
如果您的应用程序使用了很多实体bean,那么分析和配置各个bean的缓存会很麻烦。估计从“master-detail”关系的“detail”端返回的bean实例数尤为困难——比如,如果您的应用程序在“订单”表上执行一个finder操作,每个订单都有一个“项目”数可变的集合。另一个问题是由于每个实体bean都有一个单独的缓存,内存没有得到最有效的利用。认识到“每个bean一个缓存”模型的限制,WebLogic Server(从版本7开始)开始支持实体bean的应用程序级缓存。这使得同一个J2EE应用程序中的多个实体bean共享一个运行时缓存。
应用程序级缓存提供了下列好处:
为了定义应用程序级缓存,首先配置weblogic-application.xml中的entity-cache元素。然后在weblogic-ejb-jar.xml中引用entity-descriptor元素中entity-cache-ref元素中的应用程序级缓存。您可以定义一个缓存,然会将其用于应用程序中的所有实体bean,或者为bean组定义不同的缓存。也可以将应用程序级缓存与每个bean一个缓存混合使用,这样您就有很大的试验空间。我建议首先从被所有实体bean共享的应用程序级缓存开始,除非您有某些特殊需求。
使用应用程序级缓存是为每个bean指定一个缓存的可行的替代方法。可引用单个缓存的不同实体bean的数目或者已定义缓存的数目没有限制。可根据bean实例数(与每个bean一个缓存类似的方法)或者最大内存空间来指定缓存大小。从管理角度来看,使用内存的大小很有吸引力,但是要知道WebLogic Server不计算缓存中bean消耗的实际内存数(这可能是一项代价很高的操作);它只是根据weblogic-ejb-jar.xml部署描述符中规定的bean平均大小来分割内存。如果没有指定大小,则假定每个bean平均有100字节。我认为根据bean实例数指定缓存大小会更透明。
选择哪项策略?
本文已经讨论了很多内容,但是还未提及可应用于CMP bean的优化技术。比如,CMR缓存和字段组在某些环境下也很有用。选择最佳并发策略并充分利用长期缓存会给您的应用程序带来直接的性能提升。由于现在各个WebLogic Server版本中可用的选项有诸多不同(对于其他J2EE服务器也是如此),所以有时候很难在某一具体情况下做出选择,尤其是如果开发人员没有调整这些参数的经验的话。如果根本没有指定并发策略和缓存参数,WebLogic Server使用的默认设置从数据一致性方面考虑当然是不错的选择,但是从CMP bean的性能方面来看却不是最佳选择。没有放之四海皆准的东西,所以如果您不确定的话,应该分析您的用例,然后利用不同的并发设置进行测试,然后再作出最佳选择。接着,我会讨论一些基本用例以及它们的推荐设置。
静态只读数据
最可能的场景是,当数据库是静态(不随时间变化)、准静态(变化不频繁),或者从应用程序的角度来看可当作静态或准静态,并且您的应用程序不修改这些数据时。比如,数据可能被外部进程频繁更新,但是如果您的应用程序只是每分钟/小时/天看到这些更新,那么就没问题。这种情况下,使用带有适当read-timeout-seconds值的只读并发策略是合乎逻辑的。如果您的应用程序需要按照某种预定时间间隔看到更新,或者有一个批处理过程来加载数据,而你需要马上看到新数据,那么就可以像前面描述的那样,显式地禁用缓存。比如,您可以在应用程序的正面暴露一个“CMP缓存失效服务”,然后在批处理的最后或者从调度程序中调用。这种情况下缓存大小很容易计算,因为需要该缓存的所有事务共享同一个CMP实例,所以不需要考虑多版本及其对缓存大小的影响。要根据表大小、单个对象大小以及可用内存选择合理的缓存大小。
Read-mostly数据
也许最常见的情形是,数据被读取的频率大于其被更改的频率。这正是缓存可成功应用的场合。我建议使用启用了事务间缓存的乐观锁定。如果数据库模式可更改,我通常指定一个verify-columns的整型值,如果数据库模式不可更改,就指定一个Modified值。如果您决定使用一个版本列,那么要保证外部进程(如果有的话)在数据发生变更时遵守版本列更新的协定;否则,面临更新丢失的风险。
从选择适当的缓存大小方面来看,应该考虑多版本化,以及从finder方法返回的最大bean数目。一个不错的上限估计方法是将应用程序需要同时处理的最大事务数乘以单个事务可以处理的最大bean数。我通常建议使用更加灵活的应用程序级缓存,因为通常不太可能所有的CMP都同时被使用。应用程序缓存对于所有CMP来说是全局的,会自适应不同bean的活动。如果您定义了一个过大的应用程序级缓存,可能会损害性能,因为所有事务都会串行访问这个唯一的缓存。对于大小适当的缓存来说极少出现这个问题,但是同样,在您不确定缓存大小如何影响性能时应该进行性能测试。顺便说一下,良好的设计实践是,避免创建返回任意多实体bean的finder方法(比如,大型表上的findAll()),因为这使得估计出适当的缓存大小变得几乎不可能。
带有事务间缓存的乐观并发最适合有缓存碰撞“保护”的用例。比如,在一个项目用例中,应用程序需要处理传入消息(来自JMS)。每个消息记录需要在数据库表中创建,然后必须发送另一个消息作为响应;对于第二个消息来说,应用程序希望在一分钟内收到响应,并且在收到该响应时,同一条数据库记录得到更新。这时在该场景中应用缓存会带来巨大和直接的性能收益。我们“保证”每个被缓存的项目至少有一个碰撞,如果缓存对于保存CMP实例来说过大的话。
另一个极端是目标表太大,以至于不可能对同一数据作出重复请求。从实践角度来看,这是不可行的,并且缓存这样的数据不能提高性能。
在上述的read-mostly模式中,乐观并发模式应该是更好的选择。read-mostly模式不能用于集群中,不能防止出现更新丢失,并且一般来说不便于使用。本文讲述它是为了提供关于所有可用策略的整体情况,但是我不鼓励在现代应用程序中使用它。
Read-mostly数据
如果您的应用程序主要是插入或更新记录,那么缓存数据意义不大,因为几乎不会再次访问它们。在只进行插入操作(OLTP)的极端情况下,缓存反而会减慢处理速度。非重复性更新(对表中远超过缓存大小的随机行的更新)也很少从CMP缓存中受益。此外,随着更新数目相对于读取次数的增加,乐观并发策略的表现越来越差,因为会出现大量的乐观并发异常。实际上,如果您的应用程序只在数据库中更新和插入记录,就根本没有必要使用实体bean。
结束语
从本文的长度就可以看出,调整CMP 2.0 EJB有很多内容。我首先讲述了可用的各种并发策略。然后讨论了一些重要的性能策略:read-mostly模式,事务间缓存,以及选择最佳缓存大小。最后,我提供了关于在何种情况下使用何种策略的指南。我希望这些分析能帮助你更好地理解EJB。