事件对于ModeShape至关重要。当你的应用保存一些变更到页面,ModeShape就会产生描述那些变更的事件,并且会发送那些事件到你所有的被注册的应用侦听器中。不管哪些集群产生的这些变化或是集群中你的侦听器在哪一部分所监听到的,都要保证每个侦听器尽可能侦听到所有更改的事件。 但是你的应用不能仅仅只是对事件做出反应:ModeShape它自己有相当一些侦听器被允许监听和反馈那些一致的更改。许多ModeShape的侦听器在你的页面内回应那些变更,同时另外的内置侦听器也会通过ModeShape回复那些变更。怎么做到的呢?ModeShape在它的存储区域(命名空间,节点类型的定义,锁,版本,索引定义,联合规划(federated projections)等等)存储了各种类型系统的元数据。当任何的元数据被改变并且存留在集群的一个进程中,它仅会通过事件将这些变更通知到这个集群中的另外一些进程。 例如,当你的应用注册一个新的命名空间prefix/URI对时,ModeShape反射在本地命名空间(NamespaceRegistry)并注册实例至内存中的cache并且立即启动持续信息(的转发)。但是命名空间(NamespaceRegistry)的实例在其他的集群中怎么样呢?他们使用侦听器去查看在命名空间区域内系统元数据的变更,并且他们能立即观察到一个事件描述的新命名空间,(远程)命名空间(NamespaceRegistry)实例能立即更新他们的在内存中的cache,因此所有的会话自始至终与集群所看到的是一个一致的命名空间的注册集合。 ModeShape有相当多的组件,它们通过一致的方式去使用事件:索引,锁,版本,工作空间的添加/移除,全存储区的设定。 ChangeSet 和 ChangeBus要注册一个{敏感词},应用程序必须实现javax.jcr.observation.EventListener接口,然后使用工作区的ObservationManager注册一个实例。标准JCR事件可以描述在节点创建,移动或删除的基本信息,而当属性添加,更改或删除时也是。但仅此而已。 在内部,ModeShape 采用了更丰富和更细粒度类型的事件。每次提交一个事务时(不管是单个会话保存还是多个保存),所有该次提交所做的变更的描述被捆绑到单个ChangeSet。这些ChangeSet ,在ModeShape中实际上是在集群中承载的,所有的ModeShape 的内部组件被编写来响应它们,通过实现和注册内部ChangeSetListener接口的方式。有趣的是,每次你的应用程序注册一个新的事件{敏感词}的实例,ModeShape 其实注册一个内部ChangeSetListener的实现,这样不过是将每个ChangeSet(及其所描述的变更)转换成一组标准的JCREvent对象。
每个Repository实例都有一个ChangeBus组件,这个组件负责追踪所有ChangeSet{敏感词}和将所有ChangeSet引向那些{敏感词}。多个内部组件先将ChangeSet对象发送给ChangeBus,然后ChangeBus再将这些ChangeSet对象输送到每个{敏感词}的。快速准确地完成这些动作至关重要。比如,一个{敏感词}不应该干涉或妨碍其他监听。还有,一个{敏感词}应该监听到同一命令里所有发生的事件。 如果ModeShape被用作集群,ChangeBus需要满足同样的要求,但有一点不一样的:当有一个组件要发送一个ChangeSet,这个ChangeSet会马上通过JGroups被发送到集群的所有成员上,在每个进程上JGroups把这个ChangeSet对象发给ChangeBus,ChangeBus会依次将ChangeSet输送给所有本地{敏感词}。以这种方式,JGroups可以确保所有进程看到ChangeSet对象的同样的命令。 不用说,ChangeBus非常重要,也相对复杂。2.x版本中起初的设计在3.x中占据篇幅很小,但我们将在4.0版本中展示已将之彻底修补完善。 2.x 和 3.x 的 ChangeBusModeShape 2.x和3.x 使用一个相当简单的设计实现ChangeBus:每个{敏感词}有一个“消费者”的线程在不断运行,从侦听器特定阻塞先进先出队列弹出ChangeSet 对象 ,并调用实际的侦听器。当一个新的ChangeSet将被添加到总线时,ChangeBus增加该ChangeSet 到队列的前面给每一位侦听器。
每个侦听器线程 从它自己的阻塞队列消费ChangeSet 对象 这样的设计有一些很好的优点:
同时还有一些缺点:
注意:{敏感词}数量越多,对性能的影响就会越大。我们知道3.x版本已经有相当大的时间延迟。而且在4.0的早期预发布版里,我们还在3.x的基础上增加了更多内部{敏感词},并且我们还计划给每个索引提供者增加更多{敏感词}。 4.0 中新的 ChangeBus早在去年秋天,我们知道老的 ChangeBus 能够被改进并讨论了一些可能的方法。讨论的这些想法中有一个具备很大潜力:使用一个环形缓冲区。 环形缓冲区其实很简单。概念上,它可以被认为是一个单一的循环缓冲区,在一个单一的游标下,一或多个生产者可以添加条目(以线程安全的方式)到缓冲区中,消费者尾随游标和过程(每个在它们自己的线程内)访问已经保存在缓存中的每个条目。 ChangeSets 被添加在游标处,消费者线程跟在后面阅读它们。 在上图中,数字代表的是缓冲中各项的位置,从1开始,逐次增加。光标的位置是7,读取ChangeSet的每个消费者线程处在各自不同的位置:6、4、3和2 。注意:紧跟在所有消费者线程后面的是垃圾回收线程,它只是对所有消费者线程都读取了的ChangSet引用清空。(我们需要这样的线程,因为环形缓冲通常只有1024或者2048个存储项,而且假设每个环形缓冲区都有具有许多更新的ChangSet的话,就会占用大量内存。环形缓冲的垃圾回收器可以通过JVM对已经处理完成的ChangeSet对象进行回收。) 下图是另一个环形缓冲图,这张图表示的是在另外添加了7个ChangeSet对象,并且{敏感词}的消费者线程前移后的状态。
其中光标和所有的消费者线程以及垃圾回收器都已经前移。
每个消费者位置与其余消费者的位置是毫不相关的,然而,它们的位置很明显与可添加新变更项的光标位置相关。通常,{敏感词}运行的非常快,以致于消费者都紧随在光标之后。当然,也存在其他情况,比如每个ChangeSet中包含的变更数量起伏很大的时候(通常都是这样的)。
添加的ChangeSet对象越多,光标前移的就会越快,这样就有可能抵达“环形缓冲开始“的地方,此时就会开始重用缓冲区中以前已经使用的缓冲项了。(实际上,缓冲区是一个简单的预先分配了固定大小的Object[],缓冲区的位置可以很容易地被认为是数组的索引。我们只是把它想象为一个环形缓冲区。)
光标最终将会重用不再需要的缓冲区中的各项。 如果光标追上了垃圾回收器线程,那么会出现什么情况呢? 首先,环形缓冲区的大小都足够大,而且{敏感词}运行地足够快,那么这种情况就不会发生。然而,如果这种情况出现,那么环形缓冲区将阻止光标前移到垃圾回收器(通常垃圾回收器都紧随在最慢的消费者之后),或者超过。因此,添加ChangeSet对象的方法将处于阻塞状态,直到光标可以前移为止。
光标从不会“覆盖”垃圾回收器或者消费者, 因此就会自然产生压力回传。
在实际运行的内容缓冲库中,回传压力就意味着要花稍长时间进行保存操作。不过,如果这种情况出现的次数比你想象的要频繁,那么你就可以选择增大缓冲区,并重新启动内容缓冲库。事实上,这也意味着你的系统没有足够多的CPU核心去处理这么多{敏感词},或者意味着一个或者多个{敏感词}花费了太长时间,这也意味着你应当考虑使用JCR事件日志,而不是采用{敏感词}框架。(通过使用事件日志,你编写的代码可以请求某段时间内所发生的变更。) 经过上面的详细讨论,似乎环形缓冲存在大量的潜在冲突。实际上,一个良好的环形缓冲实现是在不需要使用锁技术或者同步技术就能够保持其协调一致性的。我们的实现确实做到了这样:这样的实现使用了 volatile long和比较与交换(CAS)操作来追踪光标、消费者和垃圾回收器各自所在位置,同时这样的逻辑也保证了消费者从来不会超越光标位置。事实上,我们也使用了同样的技术和代码来保证光标不会覆盖或者超过垃圾回收器线程;毕竟,缓冲区是一个有限的环形缓冲区。 当所有的消费者都追赶上光标,而且不再有 换个说法,环形缓冲区应该运行的非常快。我们研究了各种环形缓冲区的实现,其中包括LMAX Disruptor(它非常优秀)。Disruptor的大多数功能都非常的优秀,不过也有几个功能并不是很好,基于此我们很快自己编写了自己的实现。
使用LMAX Disruptor的ChangeBus实现与我们旧的实现相比速度快的就根本不再一个数量级上,然而使用我们编写的环形缓冲的实现的速度则更快些。由于我们的实现代码量小,而且集中解决我们所需,同时也不依赖第三方代码,因此我们决定把自己编写的代码优化的更强大,并集成到4.0代码库中。新的ChangeBus实现将会首先出现在在ModeShap 4.0.0.Alpha3版本里。
这篇文章有些冗长,希望你能从中找到你所感兴趣并且对你有帮助的东西。同时对一个ModeShape用户来说,也许你想更深入的了解ModeShape是如何处理事件的,其中的一种方法在ModeShape4中有了改进。
|