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”选项,可能解决更新丢失问题以及任何可能的死锁——只不过代价很高。