共计 7634 个字符,预计需要花费 20 分钟才能阅读完成。
事件对于 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 的 ChangeBus
ModeShape 2.x 和 3.x 使用一个相当简单的设计实现 ChangeBus:每个监听器有一个“消费者”的线程在不断运行,从侦听器特定阻塞先进先出队列弹出 ChangeSet 对象,并调用实际的侦听器。当一个新的 ChangeSet 将被添加到总线时,ChangeBus 增加该 ChangeSet 到队列的前面给每一位侦听器。
每个侦听器线程 从它自己的阻塞队列消费 ChangeSet 对象
这样的设计有一些很好的优点:
该设计相当简单。
每一个侦听器对 ChangeSet 中对象的相同顺序可见。
每个侦听器运行一个独立的线程,所以在大多数情况下每个侦听器都是和所有其他的侦听器完全隔离的(见下文)。
由于阻塞式队列,如果一个侦听器是真的很慢并且队列已满,那么 ChangeBus 尝试添加 changeset 到队列时将阻塞。但是这还是拖慢了系统(特别是进行更改的会话),带来了一些后端压力,尽管侦听器能跟得上。
更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2014-09/106780p2.htm
同时还有一些缺点:
当
ChangeSet
送达总线的时候,总线不得不依次向所有的监听器队列添加这个ChangeSet
,而且在从添加方法返回之前,它一直在做添加这个动作。因此,总线拥有的监听器越多,添加这个动作花费的时间就会越长。处于阻塞状态的队列都持有内部锁,在向阻塞队列添加
ChangeSet
之前必须获取到这把锁,同时消费者进程也参与获取这把锁的竞争。这就会延迟ChangeBus
的添加操作。要把最新的
ChangeSet
添加到最后一个监听器队列,只有等到把这个ChangeSet
添加到所有其他监听器队列后才可进行。这样就会在ChangeBus
接收到ChangeSet
和添加这个ChangeSet
到最后一个监听器之间产生时间差,添加ChangeSet
的监听器越是靠后,这个时间差就会越来越大(这是因为这些监听器位于监听器列表的尾部)。如果任意一个处于阻塞状态的队列已经处于满员状态(因为监听器处理
ChangeSet
s 不够快引起的),那么ChangeBus
的添加操作就会阻塞。这么做非常好(尤其是在会话状态发生变更的时候),因为它把压力回传给生产者进程,同时也要注意到:在把这个ChangeSet
添加到后续队列之前添加操作一直处于阻塞状态。因此即便后续的监听器得以运行,它们也无法意识到新添加的ChangeSet
,除非阻塞的那个监听器得以运行。因此添加到ChangeBus
的任何一个监听器依赖位于其前的所有其他监听器。每个监听器队列都会维护一份
ChangeSet
对象列表的拷贝,并且对这份拷贝进行了排序。监听器越多,队列就会越多。
注意:监听器数量越多,对性能的影响就会越大。我们知道 3.x 版本已经有相当大的时间延迟。而且在 4.0 的早期预发布版里,我们还在 3.x 的基础上增加了更多内部监听器,并且我们还计划给每个索引提供者增加更多监听器。
同时还有一些缺点:
当
ChangeSet
送达总线的时候,总线不得不依次向所有的监听器队列添加这个ChangeSet
,而且在从添加方法返回之前,它一直在做添加这个动作。因此,总线拥有的监听器越多,添加这个动作花费的时间就会越长。处于阻塞状态的队列都持有内部锁,在向阻塞队列添加
ChangeSet
之前必须获取到这把锁,同时消费者进程也参与获取这把锁的竞争。这就会延迟ChangeBus
的添加操作。要把最新的
ChangeSet
添加到最后一个监听器队列,只有等到把这个ChangeSet
添加到所有其他监听器队列后才可进行。这样就会在ChangeBus
接收到ChangeSet
和添加这个ChangeSet
到最后一个监听器之间产生时间差,添加ChangeSet
的监听器越是靠后,这个时间差就会越来越大(这是因为这些监听器位于监听器列表的尾部)。如果任意一个处于阻塞状态的队列已经处于满员状态(因为监听器处理
ChangeSet
s 不够快引起的),那么ChangeBus
的添加操作就会阻塞。这么做非常好(尤其是在会话状态发生变更的时候),因为它把压力回传给生产者进程,同时也要注意到:在把这个ChangeSet
添加到后续队列之前添加操作一直处于阻塞状态。因此即便后续的监听器得以运行,它们也无法意识到新添加的ChangeSet
,除非阻塞的那个监听器得以运行。因此添加到ChangeBus
的任何一个监听器依赖位于其前的所有其他监听器。每个监听器队列都会维护一份
ChangeSet
对象列表的拷贝,并且对这份拷贝进行了排序。监听器越多,队列就会越多。
注意:监听器数量越多,对性能的影响就会越大。我们知道 3.x 版本已经有相当大的时间延迟。而且在 4.0 的早期预发布版里,我们还在 3.x 的基础上增加了更多内部监听器,并且我们还计划给每个索引提供者增加更多监听器。
在上图中,数字代表的是缓冲中各项的位置,从 1 开始,逐次增加。光标的位置是 7,读取 ChangeSet 的每个消费者线程处在各自不同的位置:6、4、3 和 2。注意:紧跟在所有消费者线程后面的是垃圾回收线程,它只是对所有消费者线程都读取了的 ChangSet 引用清空。(我们需要这样的线程,因为环形缓冲通常只有 1024 或者 2048 个存储项,而且假设每个环形缓冲区都有具有许多更新的 ChangSet 的话,就会占用大量内存。环形缓冲的垃圾回收器可以通过 JVM 对已经处理完成的 ChangeSet 对象进行回收。)
下图是另一个环形缓冲图,这张图表示的是在另外添加了 7 个 ChangeSet 对象,并且监听器的消费者线程前移后的状态。
其中光标和所有的消费者线程以及垃圾回收器都已经前移。
每个消费者位置与其余消费者的位置是毫不相关的,然而,它们的位置很明显与可添加新变更项的光标位置相关。通常,监听器运行的非常快,以致于消费者都紧随在光标之后。当然,也存在其他情况,比如每个 ChangeSet 中包含的变更数量起伏很大的时候(通常都是这样的)。
添加的 ChangeSet 对象越多,光标前移的就会越快,这样就有可能抵达“环形缓冲开始“的地方,此时就会开始重用缓冲区中以前已经使用的缓冲项了。(实际上,缓冲区是一个简单的预先分配了固定大小的 Object[],缓冲区的位置可以很容易地被认为是数组的索引。我们只是把它想象为一个环形缓冲区。)
光标最终将会重用不再需要的缓冲区中的各项。
事件对于 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 的 ChangeBus
ModeShape 2.x 和 3.x 使用一个相当简单的设计实现 ChangeBus:每个监听器有一个“消费者”的线程在不断运行,从侦听器特定阻塞先进先出队列弹出 ChangeSet 对象,并调用实际的侦听器。当一个新的 ChangeSet 将被添加到总线时,ChangeBus 增加该 ChangeSet 到队列的前面给每一位侦听器。
每个侦听器线程 从它自己的阻塞队列消费 ChangeSet 对象
这样的设计有一些很好的优点:
该设计相当简单。
每一个侦听器对 ChangeSet 中对象的相同顺序可见。
每个侦听器运行一个独立的线程,所以在大多数情况下每个侦听器都是和所有其他的侦听器完全隔离的(见下文)。
由于阻塞式队列,如果一个侦听器是真的很慢并且队列已满,那么 ChangeBus 尝试添加 changeset 到队列时将阻塞。但是这还是拖慢了系统(特别是进行更改的会话),带来了一些后端压力,尽管侦听器能跟得上。
更多详情见请继续阅读下一页的精彩内容:http://www.linuxidc.com/Linux/2014-09/106780p2.htm
如果光标追上了垃圾回收器线程,那么会出现什么情况呢?首先,环形缓冲区的大小都足够大,而且监听器运行地足够快,那么这种情况就不会发生。然而,如果这种情况出现,那么环形缓冲区将阻止光标前移到垃圾回收器(通常垃圾回收器都紧随在最慢的消费者之后),或者超过。因此,添加 ChangeSet 对象的方法将处于阻塞状态,直到光标可以前移为止。
光标从不会“覆盖”垃圾回收器或者消费者,因此就会自然产生压力回传。
在实际运行的内容缓冲库中,回传压力就意味着要花稍长时间进行保存操作。不过,如果这种情况出现的次数比你想象的要频繁,那么你就可以选择增大缓冲区,并重新启动内容缓冲库。事实上,这也意味着你的系统没有足够多的 CPU 核心去处理这么多监听器,或者意味着一个或者多个监听器花费了太长时间,这也意味着你应当考虑使用 JCR 事件日志,而不是采用监听器框架。(通过使用事件日志,你编写的代码可以请求某段时间内所发生的变更。)
经过上面的详细讨论,似乎环形缓冲存在大量的潜在冲突。实际上,一个良好的环形缓冲实现是在不需要使用锁技术或者同步技术就能够保持其协调一致性的。我们的实现确实做到了这样:这样的实现使用了 volatile long 和比较与交换(CAS)操作来追踪光标、消费者和垃圾回收器各自所在位置,同时这样的逻辑也保证了消费者从来不会超越光标位置。事实上,我们也使用了同样的技术和代码来保证光标不会覆盖或者超过垃圾回收器线程;毕竟,缓冲区是一个有限的环形缓冲区。
当所有的消费者都追赶上光标,而且不再有 ChangeSet
对象可添加的时候,我们的实现就会让每个消费者线程阻塞,直到有 ChangeSet
对象添加为止。这种情形下,我们仅仅使用了一个简单的 Java 锁条件就做到了这些;这样的 Java 锁条件从来不会阻止添加 ChangeSet
对象。
换个说法,环形缓冲区应该运行的非常快。我们研究了各种环形缓冲区的实现,其中包括 LMAX Disruptor(它非常优秀)。Disruptor 的大多数功能都非常的优秀,不过也有几个功能并不是很好,基于此我们很快自己编写了自己的实现。
使用 LMAX Disruptor 的 ChangeBus 实现与我们旧的实现相比速度快的就根本不再一个数量级上,然而使用我们编写的环形缓冲的实现的速度则更快些。由于我们的实现代码量小,而且集中解决我们所需,同时也不依赖第三方代码,因此我们决定把自己编写的代码优化的更强大,并集成到 4.0 代码库中。新的 ChangeBus 实现将会首先出现在在 ModeShap 4.0.0.Alpha3 版本里。
这篇文章有些冗长,希望你能从中找到你所感兴趣并且对你有帮助的东西。同时对一个 ModeShape 用户来说,也许你想更深入的了解 ModeShape 是如何处理事件的,其中的一种方法在 ModeShape4 中有了改进。
ModeShape 的详细介绍:请点这里
ModeShape 的下载地址:请点这里