使用分布式缓存来群集Spring远程服务

当企业级计算进入新的SOA世界时,在寻找描述/发布/和发现服务的方面中开始变得越来越重要。基于网络服务的方案不提供自动服务发现而且通常都太繁杂了。现在新的轻量级的开发框架提供了新的轻量级的服务发布方案。

在过去几年中,Spring框架已经成为开发简单、灵活而且容易配置的J2EE应用的事实标准。Spring的核心是IoC法则。根据IoC,应用必须以一个简单JavaBean的集合来开发,然后用一个轻量级的IoC容器来绑定他们并设置相关的依赖关系。
在Spring中,容器通过一系列bean定义也配置,典型的是用XML文件方式:

  


当客户端代码需要请求时MyService,你只要如下编码:
MyServiceInterface service = (MyServiceInterface)context.getBean("MyServiceBean");
service.doSomething();


除了IoC之外,Spring提供了几百种其他服务,代码约定,而且通过回调标准API来简化开发典型的服务端应用。无论应用使用重量级的J2EE API如EJB/JMS/JMX或者使用流行的MVC框架来构建网络接口,Spring都提供了简化的效果。
随着Spring框架的成熟,越来越多的人使用他作为大型企业级项目的基础。Spring已经通过了伸缩性开发的测试而且可以作为组件粘合剂来联结复杂的分布式系统。

任何企业级应用都由各种组件组成:如联结以前的系统和ERP系统,第三方系统,网面/表示层/持久导等等。通常一个电子商务站点都是由简单的网页应用逐渐深化成包含上百个子应用和子系统的大项目,而且要面对其中的复杂性会阻碍以后的发展。通常的解决方案是将集成电路般的应用分解成一些粗纹理的服务并将其发布到网络中。

不管应用是被设计成作为分散服务的集成点或者已经集成为一体,管理所有分布式组件和其配置的任务通常都是耗时和代价高的。但如果你使用了Spring作为应用组件的开发平台,那么你就可以使用Spring的远程服务通过一系列的协议来将组件暴露给远程的客户端。通过Spring,可以使你的分布式应用就如修改一些配置文件那么简单。

在Spring中最简单的java-to-java的远程通讯方案是使用HTTP远程服务。例如,在web.xml中注册了Spring的分发服务件后,下面的上下文片断就可以将MyService作为公共接口使用了:

  
  


如你所见,实际的服务被注入到bean的定义中因此可以被远程调用:
在客户端,上下文定义如下:
      class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
               value="http://somehost:8080/webapp-context/some-mapping-for-spring-servlet/MyRemoteService" />
               value="mypackage.MyServiceInterface" />
  


通过Spring的魔法,客户端代码不需要改变,而远程方法的激活就像以前的本地调用一样。
除了HTTP远程服务外,Spring还支持其他的远程协议,如基于HTTP的解决方案(Web services, Hessian, and Burlap)和重量级的如RMI。

配置和部署基于URL的远程服务

通过基于HTTP远程服务来部署应用服务有几个明显的优点,其中一个是相对于RMI或EJB方案,你不需要担心更多的配置问题。任何尝试过使用JNDI配置(来自不同厂家的J2EE容器或者同一厂家容器的不同版本的负载均衡及群集)的人都这样认为。

URL是无格式的文本串,而这是最方便的。但同时,通过URL定义服务使得定义有些脆弱。在前面章节列举的URL的不同部分都会按照自己的方式进行变化。网络拓朴变化,负载均衡服务器代替普通服务器,应用被布署到不同机器的不同容器中,网络防火墙间的商品被打开或关闭等等。

此外,这些不稳定的URL必须被存储在每一个可能访问服务的客户端的Spring上下文文件中。当变化发生时,所有的客户端必须更新。还有从开发阶段到产品阶段的服务进程,指向服务的URL必须反映服务所在的环境。
最后我们到达了问题的关键:Spring的暴露各部分受管理的bean作为远程访问服务的能力是非常棒的。甚至在我们需要定义一个服务为服务名时,对客户端隐藏所有有关服务定位的问题。

自动发现和容错的缓存服务

这个问题最简单解决方法是使用某些命名服务来动态实时的转换服务名与服务位置。实际上,我只需要构建一次这样的系统通过使用 JmDNS类库注册Spring远程服务在Zeroconf命名空间中。

基于DNS方案的问题在于更新服务定义是不可能做到实时或事务的。一个失败的服务器在各类超时前还是出现在服务列表中。而我们需要的是快速发布并更新URL列表来实现服务并在整个网络中同步的表现所有变化。

满足这些需求的系统才是可用的。这包含各种分布式缓存的实现。对Java开发人员来说最简单的想像缓存的方式是认为缓存是一个java.util.Map接口的实现。你可以通过键值来放入一引起对象,然后你可以用同一键值取得这个对象。一个分布式缓存系统需要确保相同的键/值映射会存在于每一个参与这个缓存的服务器中的相同Map中并且步伐一致的更新缓存。

一个好的分布式缓存可以解决我们的问题。我们在实现了服务的网络中关联一个服务名和一个或多个URL。然后,我们在分布式缓存中存储name=(URL列表)关联并随着网络状态的变化(服务器的加入/移除/当机等)而相应更新。客户端访问参与分布式缓存的服务就像访问私有的服务一样。

作为附加的奖励,我们会在这里介绍一个简单的负载均衡/容错的解决方案。如果客户端知道一个服务与几个服务URL关联,他可以随机地使用其中的一个并且通过为这些URL服务的几个服务来提供自然的但也有效的负载均衡。而且,在一个远程调用失败时,客户端简单地标识那个URL不可用并且使用下一个。因为服务URL列表存储在分布式缓存中,服务器A不可用的情况也会立刻通知给别的客户端。

分布式缓存在常规的J2EE应用中非常有用,是群集服务的基础。例如,如果你有一个分布式的群集应用,分布式缓存可以在你的群集成员中提供会话复制。虽然这种方式提供了高可用性,但也存在严重的瓶颈。会话数据变化的很快,更新所有群集成员和容错的代价非常高。带有会话复制的群集应用效率通常比基于负载均衡的非会话复制的方案低很多。

在我们的案例中使用分布式缓存是因为缓存的数据很少。相对于通常有上千会话对象的分布式系统来说,我们只有少量的服务列表和对应其实现的URL。此外,我们的列表更新并不频繁。使用这样一个小列表的分布式缓存可以服务于大量的服务器和客户端。
在本文的剩余部分,我们来看一下“服务描述缓存算法”的实际实现

使用Spring和Jboss缓存来实现服务描述缓存

Jboss应用服务器可能是今天最成功的开源J2EE项目了。不管是爱是恨,Jboss应用服务器在布署服务器排行榜上占据应得的位置,而且他的模块天性使得布署更加友好。

JBoss发布包包含了很服务。其中一个是JBoss缓存。他实现的缓存提供了无论本地或远程的Java对象的高性能缓存。JBoss缓存有许多配置选项和特性,我希望你更深入的研究使得他更好的适合你的下一个项目。

对我们最有吸引的特性如下:
1、        提供了高质量的Java对象的事务复制。
2、        可以独立运行或者作为Jboss的一部分。
3、        已经是Jboss的一部分
4、        可以使用UDP多播的方式和TCP连接的方式。

JBoss缓存的网络基础是JGroups类库。JGroups提供了群体成员间的网络通讯并且可以工作于UDP或TCP方式。
在本文中,我会演示如何使用JBoss缓存来存储服务的定义和提供动态的自动服务发现。

刚开始,我们先引入一个自定义类,AutoDiscoveredServiceExporter扩展Spring的标准HttpInvokerServiceExporter类来暴露我们的TestService给远程调用:

  
  


这个在没有什么可说的。我们主要是使用他来标识Spring远程服务作为我们自己的方式来暴露。
接下来是服务端的缓存配置。Jboss包含了缓存实现,我们可以用Spring内建的JMX代理将缓存引入Spring上下文:

    
      jboss.cache:service=CustomTreeCache
    

    
      org.jboss.cache.TreeCacheMBean
    



这创建一个CustomTreeCacheMBean在服务端的Spring上下文中。通过自动代理的特性,这个bean实现了org.jboss.cache.TreeCacheMBean接口的方法。在这里,布署到Jboss服务器只需要将已经提供的custom-cache-service.xml放到服务器的布署目录下。

为了简化代码,我们引入简单的CacheServiceInterface接口:
   public void put(String path, Object key, Object value) throws Exception;
   public Object get(String path, Object key) throws Exception;


JBoss Cache是一种树状结构,这也是为什么我们需要path参数。
这个接口的服务端实现如下引用缓存Mbean:

  


在最后,我们需要ServicePublisher来观察Spring容器的生命周期,并且在我们的缓存中发布或移除服务定义:

  


这段代码显示ServicePublisher在Spring上下文刷新时(如应用补布署时)如何处理:
   private void contextRefreshed() throws Exception {
      logger.info("context refreshed");

      String[] names = context
            .getBeanNamesForType(AutoDiscoveredServiceExporter.class);
      logger.info("exporting services:" + names.length);
      for (int i = 0; i < names.length; i++) {
         String serviceUrl = makeUrl(names[i]);
         try {
            Set services = (Set) cache.get(SERVICE_PREFIX + names[i],
                  SERVICE_KEY);
            if (services == null)
               services = new HashSet();
            services.add(serviceUrl);
            cache.put(SERVICE_PREFIX + names[i], SERVICE_KEY, services);
            logger.info("added:" + serviceUrl);
         } catch (Exception ex) {
            logger.error("exception adding service:", ex);
         }
      }


如你所见,发布器简单的遍历通过缓存服务描述导出的服务列表并增加定义到缓存中。我们的缓存设计成路径包含服务名,他的URL列表存储在一个Set对象中。将服务名作为路径的一部分对JBoss Cache实现来说是重要的因为他是基于路径来创建和释放事务锁。这种方式下,对服务A的更新不会干扰对服务B的更新因为他们被映射到不同的路径:/some/prefix/serviceA/key=(list of URLs) and /some/prefix/serviceB/key=(list of URLs)。
移除服务定义的代码是类似的。

现在我们转到客户端。我们需要一个缓存实现来与服务端共享:



LocalJBossCacheServiceImpl保存着来自与服务端相同的custom-cache-service.xml配置的JBoss Cache引用:
   public LocalJBossCacheServiceImpl() throws Exception {
      super();
      cache = new TreeCache();
      PropertyConfigurator config = new PropertyConfigurator();
      config.configure(cache, "app/context/custom-cache-service.xml");
   }


这个缓存定义文件包含了Jgroups层的配置,允许所有缓存成员通过UDP多播来定位彼此。
LocalJBossCacheServiceImpl还实现了接口并且为我们的AutoDiscoveredService提供了缓存服务。这个bean扩展了标准的HttpInvokerProxyFactoryBean类但配置上有些不同:
         class="app.auto.AutoDiscoveredService">
               value="app.service.TestServiceInterface" />
      
  


最初,没有URL存在。自动在网络上寻找在TestService名字上暴露的Spring远程服务。当服务发现时,他就获得了来自分布式缓存的URL列表:
   private List getServiceUrls() throws Exception {
      Set services = (Set) cache.get(ServicePublisher.SERVICE_PREFIX
            + beanName, ServicePublisher.SERVICE_KEY);
      if (services == null)
         return null;
      ArrayList results = new ArrayList(services);
      Collections.shuffle(results);
      logger.info("shuffled:" + results);
      return results;
   }


Collections.shuffle随机地重排与服务关联的URL列表因此客户端的方法调用在他们之间是负载均衡的。实际的远程调用如下:
   public Object invoke(MethodInvocation arg0) throws Throwable {

      List urls = getServiceUrls();
      if (urls != null)
         for (Iterator allUrls = urls.iterator(); allUrls.hasNext();) {
            String serviceUrl = null;
            try {
               serviceUrl = (String) allUrls.next();
               super.setServiceUrl(serviceUrl);
               logger.info("going to:" + serviceUrl);
               return super.invoke(arg0);
            } catch (Throwable problem) {
               if (problem instanceof IOException
                     || problem instanceof RemoteAccessException) {
                  logger.warn("got error accessing:"
                        + super.getServiceUrl(), problem);
                  removeFailedService(serviceUrl);
               } else {
                  throw problem;
               }
            }
         }
      throw new IllegalStateException("No services configured for name:"
            + beanName);
   }


如你所见,如果远程调用抛出异常,客户端代码可以处理这个问题而且可以从列表中取下一个URL,因此也就提供了透明的容错性。如果调用因为某些异常失败了,他为重新抛出异常给客户端处理。
下面的removeFailedService()方法简单的从列表中移除了失败的URL并更新分布式缓存,使这个信息同步地通知所有其他客户端:
   private void removeFailedService(String url) {
      try {
         logger.info("removing failed service:" + url);
         Set services = (Set) cache.get(ServicePublisher.SERVICE_PREFIX
               + beanName, ServicePublisher.SERVICE_KEY);
         if (services != null) {
            services.remove(url);
            cache.put(ServicePublisher.SERVICE_PREFIX + beanName, ServicePublisher.SERVICE_KEY,
                  services);
            logger.info("removed failed service at:" + url);
         }
      } catch (Exception e) {
         logger.warn("failed to remove failed service:" + url, e);
      }
   }


如果你构建并布署一个样例应用在多个Jboss服务器上而且运行提供的LoopingAutoDiscoveredRemoteServiceTest,你可以看到请求是如何在Spring群集中负载均衡的。你也可以停止和重启任何的服务器,而调用会动态地路由到其他的服务器上。如果你当掉一台服务器,你会看到一个异常被输出到客户端的控制台上,但所有的请求依旧无停顿的传递给其他服务器。

小结
在本文中,我们了解了如何通过Spring的远程服务来群集网络服务。此外,你可以学到如何通过只使用名字来定义私有的服务及依赖自动发现来绑定服务到相应的URL,从而简化布署一个复杂的多层应用

关于作者
Mikhail Garber是达拉斯信息技术专家,拥有14年的企业级软件开发经验。精通Java/J2EE,数据库,消息及开源解决方案。他的方案被多家组织采用,如Mary Kay Cosmetics, Boeing Defense and Space, Verizon Wireless, the US government, Lockheed Martin, Sabre/Travelocity等。

Resources
本文代码: http://www.javaworld.com/javaworld/jw-10-2005/spring/jw-1031-spring.zip
Spring Framework and Spring remoting: http://www.springframework.org
JBoss Cache: http://www.jboss.org/products/jbosscache

你可能感兴趣的:(Spring,JAVA)