Apache Jackrabbit内部的并发模型相当复杂,在JackRabbit 1.x发布周期内报告并修复了大量的死锁问题。这篇文章是设计和code review的成果,旨在主动预防其他类似的问题
此文是关于Jackrabbit的内部并发和同步机制模型,不是关于JCR的锁特性。注意此文是从架构层面审阅并发控制,并不关注像个别类或组件的多线程安全之类的问题。
此review是基于JackRabbit 1.5版本的默认版本
架构背景
从并发控制方面,Jackrabbit 架构大概可以分成下面的五个层次
1.集群
2.仓库
3.工作空间
4.会话
5.事务
- 集群 集群层需要在一个或者多个集群节点恰巧共享它们的内容时,这些节点可以视为独立的仓库,处理这些同步更改。并发控制跨越多个集群节点是通过使用一个单一的写入锁来实现的,这锁是节点在提交任何更改到共享状态时都需要先获取到的。另一方面所有的集群节点都可以在不需要任何明确的同步措施下并行读取已经共享的内容。注意既然所有的集群节点共享一把锁,那么在节点之间就不会发生任何死锁的情况。一个发生死锁的节点仍然会潜在的阻碍写入整个集群,但是如果每个节点本身都是无死锁的,那么集群功能就不会添加任何新的死锁场景
- 仓库 仓库层负责所有全局仓库状态比如节点类型注册和版本存储。所有仓库范围的组件都有他们自己的同步机制而不是共享一把全局仓库锁。从并发控制视角来看最值得注意的组件就是版本存储,实际上它有两种锁机制;一个VersionManagerImpl负责高层次的版本操作,另一个底层的SharedItemStateManager为了控制访问底层的持久化机制。
- 工作空间 仓库由一个或者多个工作空间组成,工作空间包含这个仓库中正常的内容树状结构。每一个工作空间由一些像持久化机制和查询索引组成。持久化机制通过Shared ISM(共享项状态管理器)和PersistenceManager(持久化管理器)建立的,Shared ISM会控制所有项的操作,PersistenceManager把所有项进行永久存储。大多数的持久化管理使用Java同步和其他锁机制进行并发控制,但是因为它们通常不会与仓库的其他部分相互影响,所以从全局并发角度来看它们并不是关键。另一方面,使用读写锁的SharedISM是关键因素尤其它是与仓库范围的版本存储交互的途径。注意自从Jackrabbit 1.4就可以配置SharedISM的锁策略,通过使用更细粒度的锁配置,允许并发写访问内容树的不同部分。本文关注于默认情况只有一个简单的SharedISM锁,但是从锁角度来看更细粒度的情况相当于拥有更多工作空间因此本文的结论仍然是适用的。
- 会话 每一个工作空间可以被零个或多个JCR 会话访问。每个会话拥有一个暂时空间来存储在会话中尚未保存的更改。因为暂时空间是在会话的内部(其他会话不可见)和会话应该每次只被一个线程访问,我们几乎不用关心并发和会话使用会有关联。可是值得注意在许多情况下会话的线程安全需求,Jackrabbit并没有明确地保证,所以一个客户端无论是有意还是无意地在多并发线程中使用单独的一个会话可能会破坏会话的内部状态。
- 事务 Jackrabbit处理事务是通过把所有项的操作(如保存暂时更新和直接工作空间更新以及版本和锁操作)封装到一个可以说是更大的暂时空间,当事务被提交时再把它持久化。在Jackrabbit中没有事务锁,但是事务支持仍然从根本上改变了Jackrabbit并发控制比如它基本上通过使用更大的提交操作取代了所有的写操作(还有相关的锁)。只有在XA事务的环境下,事务模式才会被激活。