技术开发 频道

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

  使用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给远程调用:

<bean name="/TestService" class="app.service.AutoDiscoveredServiceExporter"> <property name="service" ref="TestService"/> <property name="serviceInterface" value="app.service.TestServiceInterface"/> </bean>

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

<bean id="CustomTreeCacheMBean" class="org.springframework.jmx.access.MBeanProxyFactoryBean"> <property name="objectName"> <value>jboss.cache:service=CustomTreeCache</value> </property> <property name="proxyInterface"> <value>org.jboss.cache.TreeCacheMBean</value> </property> </bean>

 这创建一个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:

<bean id="CacheService" class="app.service.JBossCacheServiceImpl"> <property name="cacheMBean" ref="CustomTreeCacheMBean"/> </bean>

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

<bean id="ServicePublisher" class="app.service.ServicePublisher"> <property name="cache" ref="CacheService"/> </bean>

 这段代码显示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)。

  移除服务定义的代码是类似的。

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

<bean id="LocalCacheService" class="app.auto.LocalJBossCacheServiceImpl"> </bean>

  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类但配置上有些不同:

<bean id="TestService" class="app.auto.AutoDiscoveredService"> <property name="serviceInterface" value="app.service.TestServiceInterface" /> <property name="cache" ref="LocalCacheService"/> </bean>

  最初,没有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,从而简化布署一个复杂的多层应用。

 

0
相关文章