深入理解JBoss Cache3.0——Naga

原文请看: http://java.dzone.com/articles/a-look-inside-jboss-cache




作者 Manik Surtani 是开源项目 JBoss Cache 的领导人,本文主要是对 JBoss Cache3.0 —— Naga 一些全新技术的论述,其中也不乏对原有技术的回顾。总的来说,本文还是揭示了缓存的未来—— MVCC ,值得推荐。


正文




当我在 DZone 写完《 [url=http://java.dzone.com/articles/caching-parallelism-scalability]分布式,缓存与并行[/url] 》一文后,许人多次我为什么这么“厚着脸皮”的力挺我领导的 JBoss Cache 开源项目时,我想本文会给你们一个满意的答案。这篇文章将完全致力于 焕然一新的 JBoss Cache3.0 (代号 Naga ),并且深入讨论我们的高性能并发模型。







Naga 的今生前世


在开始讨论 Naga 之前,我还是想简要的说说 JBoss Cache 的历史。大概在 2002 的时候,为了维持集群的高可用性, JBoss AS ( JBoss 应用服务器)需要提供一个专门为解决 HTTP 和 EJB Session 状态复制的集群缓存方案 。 JGroups 是一款开源的成组通信 (group communication ) 项目。 Bela Ban 是 JGroup 的创始人也是维护开发人员,对 JGroup 进行扩展,使其适应树形数据结构 ,并且还增加了一个缓存相关的特性: eviction 和 JTA Transactions 。大约在 2003 年年初时,这个被扩展的树型结构迁移到了 JBoss AS CVS 的 repository 中,从此成为 Jboss AS 中的一员。





时间齿轮又指向了 2005 年 3 月, JBoss Cache 从 JBoss AS 的 repository 中分离出来,单独形成一个项目。唉,那都是陈年旧事了。不过,像 cache loading( 缓存负载 ) ,多种缓存装载器的实现, eviction 策略, buddy replication(buddy 复制 ) 都是后来慢慢加入的。基于 TCP 的委托缓存服务器允许你构建多层缓存。当进行状态提制时, Custom marshalling 框架为比 Java 序列化机制提供了更高的性能。紧接着又迎来了 JBoss Cache2.0 的发布,这次的 API 改动很大,并且要求基于 Java5 。另外两个基于此核心缓存技术的 POJO版本 和 Searchable版本 也发展良好。







火炬交接





现在,是将 Jboss Cache2.* 系列的火炬转交给 Naga 了。这次 Naga 又有了很大的变化和改进, 除了资源管理和 marshalling 的全面提升,以及全新的简化配置文件格式外,它还包括至少一个革命性的改变: MVCC 。









MVCC 时代已经到来



MVCC 全称 Multi-versioned concurrency control( 多版本并发控制 ) ,在 Naga 中已经被采纳作为默认的并发解决方案。





当以本地方式运行时,对内存和 CPU 而言,缓存最大的开销就是使用锁来在保证共享数据完整性。而到了集群环境中,锁成了继 RPC 调用后的第二大开销“大户”。









对遗留的锁机制回顾


在 JBoss Cache1.* 和 2.* 时代,我们提供两种不同的锁方案——即乐观锁和悲观锁。它们各有千秋,但是从性能角度上来说,它们还是开销太大。





悲观锁用来锁住树中的每个结点。 Reader threads (读线程)可以得到一个非独占的 read locks( 读锁 ) ,而 writer threads( 写线程 ) 却可得到一个独占的 write locks( 写锁 ) ,从而独占这些结点。我们实现的锁是通过扩展 JDK 的 ReentrantReadWriteLocks ,将其改进成为支持事务作用域内的锁更新——即一个线程可以开始时用 read locks 去读取一个结点,稍后再尝试用 write locks 着去更新它。(注意,悲观锁的读写是互斥的,无法同时进行的 )





总得来说,这种方案简单而且健壮,但由于内存要维护每个被锁的结点,所以从性能上说还不是很满意。更重要的是,如果结点已经被 read locks 锁住了,那么 write locks 就没办法再去操作它们了,使得并发性能下降。读操作阻塞写操作的后果还容易造成死锁。好吧,我们现在来看个例子:



“事务 A 提交前,对结点 /X 执行读操作,对结点 /Y 执行写操作。事务 B 恰恰与之相反,在提交前,对结点 /Y 执行读操作,对结点 /X 执行写操作。不幸的事发生了,事务 A 对结点 /X 用了 read lock ,并且还在等待时机去用 write lock 操作 /Y 结点;而事务 B 对结点 /Y 用了 read lock ,也还在等待时机去用 write lock 操作 /X 结点。这两个事务发生死锁了,直到其中的一方超时,然后事务回滚。”





为了克服潜在的死锁问题,我们提供了乐观锁。乐观锁对每个结点采用版本控制方式。它允许任意多个结点拷贝 (Nodes copied) 出现在一个事务中,并允许事务处理这些拷贝。结点拷贝为读操作提供了可重复读取的语义,同时还允许 writer threads 在不考虑读操作的情况下,进行相应的写操作。那些修改的结点会在事务提交时进行版本检查,确保没有新的并发写操作发生,最后将结点合并到缓存的 main tree 上去。





乐观锁提供了更高级别的并发机制来处理并发读写操作,而且还避免了死锁的风险。但它仍然有两个主要的缺点:一是性能问题。因为不断的将结点的状态拷贝到每个并发线程所造成的内存和 CPU 开销是不容忽略的。二是尽管并发时允许了写操作,但是一旦发现数据的版本不对,事务提交时不可避免的还是会失败。也就是说,此时写事务虽然可以不受限制的进行大量处理和写操作,但是这样在事务结束的时候容易出现提交失败。









MVCC 有什么用





MVCC 提供了非阻塞 (non-blocking) 读操作 ( 它并不会去阻塞 wirter threads) ,在避免死锁的同时也提供了更高级的并发机制。它采用了 fail-fast 机制,如果写操作得到了一个 write lock ,那么它们也是依次进行,不允许重叠。最后我要说的是, MVCC 在内存使用率上也是可圈可点:它对所有的读操作只维护一个状态的拷贝;对依次顺序进行的写操作来说,每次的修改只会对版本号产生一次变化。更棒的是,我们的 MVCC 实现甚至可以对 reader threads 完全不采用任何锁 ( 对于像缓存这样频繁读取的系统来说,意义太大了 ) ,并且还允许自定义的为写操作实现独占锁。自定义锁完全摒弃了同步代码块,使用了最新的并发技术: compare-and-swap 和 memory fencing( 使用 volatile variables 实现同步 ) 。所有的这一切都会让 MVCC 在性能和可伸缩性方面,成为一个更加出色的解决方案。





说了这么多,是该谈 MVCC 的细节了。





JBoss Cache 的 MVCC 实现这所以这么高效在于 reading threads 之间不需要任何同步代码块或锁机制。对于每个 reader thread 来说,缓存将结点的状态包装在一个轻量级的容器对象(比如说 ThreadLocal )或者长事务中。所有的后继操作要想访问或操作缓存中的结点,都必须通过这个容器对象。甚至当结点的状态真的在并发时发生了变化,那么使用 Java 引用的使用也可以达到可重复读取的语义。(下文有具体的说明 )





从另一方面来看, write threads 首先需要获得一个锁后,才可执行写操作。现在,我们的做法是使用 lock striping (分离锁)来提升缓存的内存性能,而 shared lock pool( 共享锁池 ) 级别可以使用被锁定结点的 concurrencyLevel 属性来进行调整 ( 更多细节,请看 [url=http://www.jboss.org/community/docs/DOC-12843]Jboss Cache的配置参考[/url] ) 。在获得一个独占锁后,如同 reader threads 那样, writer thread 也会将要修改结点的状态包装在一个容器中,然后将它的状态拷贝出来,再进行写操作。注意,在拷贝状态的时候,指向原始结点的引用仍然是可以进行回滚操作的。当写操作即将完成时, writer thread 最终又将已经发生改变的拷贝的状态写回相应的数据结构中(比如说文件系统,数据库等,但是始终不会影响到在容器中的原始结点,感觉与 oracle 机制有点像) ,最后操作完成。





这样的话,假如一些现有的 reader thread 再次读取该结点时,发现其版本号已经更新了,它仍然会持有指向原来结点的引用,从而实现可重复读取的语义。





如果写操作在等待一定时间后,仍然无法获取 write lock 的话,一个 TimoutException 立即抛出。





尽管 MVCC 已经强行要求写操作必须先获得一个 write lock ,但是众所周知,即使是使用可重复读这一隔离级别,由于 write skew( 写偏斜 ) 所造成的幻读仍然有可能发生。当并发事务进行读写操作时,由于读操作会一直在事务上下文中持有结点的原始引用,那么就算接下的写操作就算已经将该结点给处理掉了,对于读操作来说也是透明的,那么幻读就发生了。











在拷贝结点状态准备进行写操作时,如果检测到 write skew ,那么默认的处理方式就是抛出一个 DataVersioningException 异常。尽管如此,对大多数并不苛刻的应用程序来说, write skew 也许不是什么问题而且出现这样的情况也是允许的。如果你的应用程序并不关心 write skew ,你可以将 writeSkewChecks 属性设置为 false ,完全不予理睬。看看文档,里面有关于 Jboss Cache 的配置细节。





需要注意的是,如果设置了 READ_COMMITTED 隔离级别时,线程总是会处理已经提交的结点,那么 write skews 就可以避免发生;当使用乐观锁时,无论使用何种隔离级别, write skews 都有抛出 DataVersioningException 异常的可能。







有什么可参考的 tutorial 吗?


当然。你们只要下载 JBoss Cache 的 bosscache-core-all.zip 分发包,里面就有图文并茂的 tutorial 来帮你实现自己的缓存。 这里还有一个 GUI的 deom来更好的说明缓存 。









下载地址





总而言之, Naga 是 JBoss Cache 最新也是最好的缓存产品了。无论对于读操作还是写操作,它都将提供更快速的访问方式;对于稳定与性能方面也将更加优异;更快速的复制,更少的内存消耗都可以让为你节省更多的时间。 下载 Naga请点击这儿 , FAQ和 tutorial在这里下载 。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、利用“naga-2_1-r42.jar”包进行二次开发; 2、调用NIOService类中的打开socket和serverSocket函数进行编程; 3、例如,服务器端: NIOService service1 = new NIOService(); NIOServerSocket serverSocket = service1.openServerSocket(port); // 设置监听事件,包括打开监听后,关闭后这两个: serverSocket.listen(new ServerSocketObserverAdapter() { public void newConnection(NIOSocket nioSocket) { // 可在此处添加处理事件 } public void serverSocketDied(Exception exception) { // 关闭serverSocket处理 } }); // 还需要一个循环事件,执行事件监听,此处可能需要使用到多线程编程 // 设置接收链接方式 serverSocket.setConnectionAcceptor(ConnectionAcceptor.ALLOW); while (true) { // 循环不断监听事件 service1.selectBlocking(1); } 4、又例如,客户端,包括链接上,接收包,关闭链接后三个事件和write()方法: NIOService service2 = new NIOService(); NIOSocket socket = service2.openSocket(InetAddress.getLocalHost(), port); // 设置接收包格式 nioSocket.setPacketReader(new RegularPacketReader(1, true)); nioSocket.setPacketWriter(new RegularPacketWriter(1, true)); nioSocket.listen(new SocketObserverAdapter() { public void connectionBroken(NIOSocket nioSocket, Exception arg1) { // 打开后处理 System.out.println("Client ip:" + nioSocket.getIp() +" port:"+nioSocket.getPort()+ " disconnected."); nioSocket.close(); isConnected = false; } public void connectionOpened(NIOSocket nioSocket) { // 关闭链接后处理 System.out.println("Client ip:" + nioSocket.getIp() +" port:"+nioSocket.getPort()+ " connected."); } public void packetReceived(NIOSocket nioSocket, byte[] packet) { revNum = nioSocket.getBytesRead(); // 包处理 revDataAndParse(packet); }}); // 还需要一个循环监听事件并执行,此处可能需要使用到多线程编程 while (true) { service2.selectBlocking(1); } 5、该项目开发过程基本上是以上3、4点的封装过程,利用接口编程,实现一个客户端基础类(TCPClientBase) 和服务器端基础类(TCPServerBase),其中客户端基础类有两种方式,一种是使用于单独的客户端的单元;另 一种使用与服务器端接收一个链接后的单元; 6、项目类使用: 1) communication.tcp.client.TCPClientBase是基础抽象类,任何外部使用都必须继承该类,并重写一 个“protected abstract void revDataAndParse(byte[] data);”方法,建议外部重写时,设置成protected&private 以防止类外部调用。同样在继承该基础类时,可以在继承类中添加各种需要的引用及必要的方法,实例在 communication.tcp.example.TCPClientUnit体现,重写revDataAndParse方法如下: /** * 处理数据将数据置于队列中,或者直接解析....... */ protected void revDataAndParse(byte[] data) { // SystemOut.arrayToHexString(data); // sendData(data); // System.out.println(new String(data)); } 2) communication.tcp.server.TCPServerBase也是基础抽象类,任何外部使用都必须继承该类,并重写至少包括一些内容: /** * 服务器端构造方法 * @param ip * @param port * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") public TCPServerUnit(String ip, int port) throws ClassNotFoundException { // 注意保持与泛型中的类一致性 super(ip, port,(Class<TCPClientUnit4Server>) Class.forName(TCPClientUnit4Server.class.getName())); } 7、弊端:程序为未经过严格的验证,使用效果如何未知;客户端中的接收包有一定的局限性,接收数据包不利于开发解析,对于实时性要求比较 高的系统不一定适用,如modbusTCP。 8、详细使用过程,请参考doc目录中的api帮助文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值