智能客户端

使用 NHibernate 和 Rhino 服务总线构建分布式应用程序

Oren Eini

有很长一段时间,我的工作内容几乎都是 Web 应用程序。当我要构建一个智能客户端应用程序时,起初我觉得非常困惑,不知该如何构建这样的应用程序。怎么处理数据访问?智能客户端应用程序与服务器之间如何通信?

而且,我那时已经投入很多,拥有一些能够显著减少开发时间和成本的工具,而我真的希望可以继续使用这些工具。我花了一段时间来深入考虑各种细节问题,在这期间,我一直在想如何让 Web 应用程序更简单些呢,当然我需要先知道如何处理这样的应用程序。

智能客户端应用程序有利有弊。从有利的一面看,智能客户端反应灵敏,能够改进与用户的交互性。如果您将处理移到客户端计算机,还能减轻服务器的负载,而且即使与后端系统断开连接,用户也能照常工作。

另一方面,智能客户端又有一些固有的问题,包括通过 Intranet 或 Internet 访问数据时需要解决速度、安全性和带宽限制等问题。您还要负责在前端与后端系统之间实现数据同步、处理分布式的更改跟踪,以及处理在偶尔连接的环境中使用时出现的问题。

本文中讨论的智能客户端应用程序可以使用 Windows Presentation Foundation (WPF) 或 Silverlight 构建。由于 Silverlight 具备一部分 WPF 功能,因此我在此处介绍的技巧和方法同时适用于这两种工具。

在本文中,我将使用 NHibernate 进行数据访问,使用 Rhino 服务总线实现与服务器之间的可靠通信,从而开始规划和构建智能客户端应用程序的过程。我要构建的应用程序名为 Alexandra,是一个在线借阅图书馆的前端系统。该应用程序本身分成两大部分。第一部分是运行了一组服务的应用程序服务器(大部分的业务逻辑都在此服务器上),使用 NHibernate 访问数据库。第二部分是智能客户端 UI,让用户轻松使用上述那些服务。

NHibernate 是一个对象关系映射 (O/RM) 框架,旨在让用户轻松使用关系数据库,就像使用内存中的数据一样。Rhino 服务总线是构建在 Microsoft .NET Framework 上的开源服务总线,其主要目的是让部署、开发和使用变得轻松自如。

职责的分配

构建借阅图书馆的第一步是正确分配前端系统和后端系统各自的职责。一种途径是让应用程序主要负责 UI,以便大部分的处理都在客户端计算机上进行。在这种情况下,后端服务器几乎就是一个数据存储库。

从本质上说,这就是传统的客户端/服务器应用程序,后端系统只是作为数据存储的代理。如果后端系统只是一个数据存储库,这不失为一种有效的设计选择。例如,这种体系结构可能很适合个人图书目录,因为这种应用程序的作用就是为用户管理数据,服务器端不需要对数据进行操作。

对于这样的应用程序,我建议您使用 WCF RIA 服务或 WCF 数据服务。如果您希望后端服务器向外界提供一个 CRUD 接口,则使用 WCF RIA 服务或 WCF 数据服务能让您显著减少构建应用程序所需的时间。尽管这两种技术都允许您将自己的业务逻辑添加到 CRUD 接口中,但是任何通过添加逻辑来实现重要应用程序功能的尝试最终都可能导致不可收拾的混乱局面。

我在本文中不会探讨如何构建这样的应用程序,不过 Brad Adams 在他的博客 blogs.msdn.com/brada/archive/2009/08/06/business-apps-example-for-silverlight-3-rtm-and-net-ria-services-july-update-part-nhibernate.aspx 中详细介绍了使用 NHibernate 和 WCF RIA 服务构建这种应用程序的操作步骤。

与上述完全不同的方法是,您可以选择在后端实现应用程序的大部分功能,而前端只是负责显示。这种方法初看起来似乎是合理的,因为通常您就是这样编写基于 Web 的应用程序的,但这也意味着您不能在客户端运行真正的应用程序。状态管理将会变得更困难。从本质上说,您仍是在编写 Web 应用程序,其所有复杂问题都不可避免。您将无法把处理转移到客户端计算机,也不能处理连接中断的问题。

更糟的是,从用户角度而言,这种方法意味着您展示的 UI 很迟钝,因为所有操作都需要先传递到服务器再返回。

我敢肯定,我在本例中采用的方法居于上述二者之间,这件事不会让您感到惊讶。我要在客户端计算机上运行以充分利用由此带来的各种机会,但同时,应用程序的大部分功能将作为服务在后端运行,如图 1 所示。

图 1 应用程序的体系结构

该示例解决方案包括三个项目,您可以从 github.com/ayende/alexandria 下载。Alexandria.Backend 是包含后端代码的控制台应用程序。Alexandria.Client 包含前端代码,Alexandria.Messages 包含在前两者之间共享的消息定义。若要运行该示例,Alexandria.Backend 和 Alexandria.Client 都需要运行。

将后端托管在控制台应用程序中的一个好处就是,您可以方便地模拟连接断开时的情景:只需先关闭后端控制台应用程序,之后再重新打开。

有关分布式计算的谬论

有了体系结构的基本框架,我们来看看编写智能客户端应用程序的含义。与后端的通信将通过 Intranet 或 Internet 进行。考虑到大多数 Web 应用程序中的远程调用主要来自位于同一数据中心(通常就在同一机架中)的数据库或另一个应用程序服务器,因此这将严重影响好几个含义。

Intranet 和 Internet 连接会受到速度问题、带宽限制和安全性的制约。如果应用程序的所有重要组成部分都在同一个数据中心内,通信成本的巨大差距将迫使您选择与现行通信结构完全不同的结构。

在分布式应用程序中,您需要处理的最大障碍就是有关分布式计算的谬论。开发人员在构建分布式应用程序时往往会提出若干个假设,而最终这些假设都不成立。依靠这些根本不成立的假设(谬论)通常会导致功能减少,或者导致成本非常高的系统重新设计和重新构建。以下就是八条谬论:

  • 网络稳定可靠。
  • 没有丝毫延迟。
  • 带宽是无限的。
  • 网络是安全的。
  • 拓扑不会改变。
  • 有一位管理员。
  • 传输成本是零。
  • 网络是同构的。

不考虑这些谬论的分布式应用程序都将遇到服务器问题。因此智能客户端应用程序需要解决这些问题。在这种情况下,使用缓存就成为一个重要手段。即使您不打算让应用程序在连接断开时仍能运行,缓存对于提升应用程序的响应性也是很有用的。

另一个需要考虑的事项就是应用程序的通信模型。可能最简单的模型就是标准的服务代理,该代理允许您执行远程过程调用 (RPC),但这样做可能会在将来造成问题。它需要使用更复杂的代码来处理连接断开的情况,如果您要避免阻塞 UI 线程,则还要显式处理异步调用。

后端基础知识

接下来的问题就是如何采用合适的方法来构造应用程序后端,以便既能获得良好的性能,又能与 UI 的结构有适当的分离。

从性能和响应性的角度而言,理想的方案是只需对后端执行一次调用,就能为正在显示的屏幕获得所有必要的数据。这样做的问题就是您需要一个能够精确模拟智能客户端 UI 的服务接口。出于许多原因,这不是明智的做法。最主要的原因就是 UI 是应用程序中最易变的部分。以这种方式将服务接口绑定到 UI 会导致频繁更改服务,而这些更改仅仅是因为 UI 发生了变化。

而频繁更改服务又会导致应用程序的部署变得更困难。您必须同时部署前端和后端,而尝试在同一时刻支持多个版本可能会导致情况更加复杂。此外,该服务接口无法用来构建其他 UI 或者作为第三方或其他服务的集成点。

如果您尝试使用其他方法(即构建标准的、精细的接口),您就会遇到谬论问题(精细的接口会产生大量远程调用,从而导致延迟、可靠性和带宽问题)。

解决这个问题的方案就是脱离常见的 RPC 模型。我们不提供可以远程调用的方法,而是使用本地缓存和面向消息的通信模型。

图 2 显示了如何从前端向后端打包发送几个请求。这样您就可以进行一次远程调用,但在服务器端保留一个与 UI 需求的联系并不紧密的编程模型。

图 2 发送给服务器的一个请求包含多条消息

要增强响应性,您可以加入一个可以立即应答某些查询的本地缓存,从而获得反应更灵敏的应用程序。

在这些方案中,您要考虑的事情之一就是您有什么类型的数据,以及要显示的数据的刷新要求。在 Alexandria 应用程序中,我主要依靠本地缓存,因为本地缓存在应用程序向后端系统请求最新数据期间,可以为用户显示缓存的数据。而其他应用程序(例如股票交易程序)可能应该不显示任何数据(不能显示陈旧的数据)。

连接断开时的操作

您面临的下一个问题是处理连接断开的情况。在很多应用程序中,您可以指定连接是强制的,这意味着当后端服务器不可访问时,您只需为用户显示一个错误。但智能客户端应用程序的优势之一就是它可以 在连接断开时继续运行,Alexandria 应用程序就充分利用了这一点。

但是,这意味着缓存变得更加重要,因为既要使用它来提高通信速度,还要在后端系统不可访问时利用它来提供数据。

到目前为止,我相信您已经很好地了解了构建这样的应用程序所面临的问题,现在让我们看看如何解决这些问题。

队列是我的最爱之一

在 Alexandria 中,前端和后端之间不存在 RPC 通信。相反,正如图 3 所示,所有的通信都通过排入队列的单向消息进行处理。

图 3 Alexandria 的通信模型

队列为解决前述的通信问题提供了一种相当不错的解决方式。前端和后端之间无需直接通信(直接通信意味着很难 支持连接断开的情况),您可以让队列子系统处理所有通信。

使用队列非常简单。您让本地的队列子系统向某个队列发送消息。队列子系统拥有消息的所有权,并确保消息在某一时刻到达其目标。但是您的应用程序并不需要等待消息到达其目标,而可以继续其工作。

如果目标队列目前不可用,则队列子系统会等待直到目标队列变为可用,才会发送消息。队列子系统通常会将消息保存在磁盘上直到可以发送消息,因此即使源计算机已经重新启动,待定的消息还是能够到达其目标。

使用队列时,可以轻松地构思消息和目标。到达后端系统的消息将触发某种操作,从而导致回复被发送给原始消息发送方。请注意,无论哪一方都不会被阻塞,因为每个系统都是完全独立的。

队列子系统包括 MSMQ、ActiveMQ、RabbitMQ 等等。Alexandria 应用程序使用了 Rhino Queues (github.com/rhino-queues/rhino-queues),这是一个通过 xcopy 部署的开源队列子系统。我选择 Rhino Queues 的原因很简单,它不需要安装或管理工作,因此非常适合用在示例中以及用在需要部署到许多计算机的应用程序中。还要请您注意的是,Rhino Queues 的作者是我,希望您会喜欢它。

开始使用队列

让我们看看您如何使用队列为主屏幕获取数据。以下是 ApplicationModel 初始化例程:

protected override void OnInitialize() {
  bus.Send(
    new MyBooksQuery { UserId = userId },
    new MyQueueQuery { UserId = userId },
    new MyRecommendationsQuery { UserId = userId },
    new SubscriptionDetailsQuery { UserId = userId });
}

我向服务器发送一批消息,请求若干信息。此处需要注意几个事项。发送消息的频率很高。我并没有发送一条通用的消息(例如 MainWindowQuery)而是发送了很多消息(MyBooksQuery、MyQueueQuery 等等),每一条消息都是为了获得一条具体的信息。正如前文所论述的,这样做既可以一次性发送多条消息以减少网络往返,还可以降低前端和后端的联系紧密度。

RPC 是您的敌人

构建分布式应用程序时最常见的错误之一就是忽略应用程序的分布性。以 WCF 为例,就很容易使人忽略需要通过网络进行方法调用。尽管 WCF 是非常简单的编程模型,但您需要特别小心,不要违反有关分布式计算的某个谬论。

事实上,正是由于像 WCF 这样的框架提供的编程模型非常像您在本地计算机上调用方法时使用的模型,才会让您做出那些错误的假设。

如果使用标准的 RPC API,则意味着当通过网络进行调用时会出现阻塞、远程方法调用的成本会更高以及如果后端服务器不可用则可能失败。虽然在此基础上完全有可能构建一个优秀的分布式应用程序,但需要格外小心谨慎。

如果采用别的方法,您将会用到基于显式消息交换的编程模型(即与大多数基于 SOAP 的 RPC 堆栈中常用的隐式消息交换相反)。这样的模型初看起来可能很奇怪,其实您只需要稍微调整一下思路,就会发现您要担心的复杂问题会大大减少。

我的示例应用程序 Alexandria 就构建在单向消息传送平台上,并充分地利用了这个平台,因此我的应用程序知道自己是分布式的,并真正了利用了这一点。

请注意,所有的消息都以 Query 这个词结尾。我用这样的命名约定来标识纯粹的查询消息,这种消息不会改变状态,但需要某种类型的响应。

最后还需要注意的是,看起来我并未从服务器获得任何回复。因为我使用了队列,因此通信模式是发出消息后就不加处理。我现在发出一条消息(或一批消息),要在后面的阶段才处理回复。

在介绍前端如何处理响应之前,我们先看看后端如何处理我刚才发送的消息。图 4 显示了后端服务器如何使用图书查询。您将在这里第一次看到我如何结合使用 NHibernate 和 Rhino 服务总线。

图 4 在后端系统上使用查询

public class MyBooksQueryConsumer : 
  ConsumerOf<MyBooksQuery> {

  private readonly ISession session;
  private readonly IServiceBus bus;

  public MyBooksQueryConsumer(
    ISession session, IServiceBus bus) {

    this.session = session;
    this.bus = bus;
  }

  public void Consume(MyBooksQuery message) {
    var user = session.Get<User>(message.UserId);
    
    Console.WriteLine("{0}'s has {1} books at home", 
      user.Name, user.CurrentlyReading.Count);

    bus.Reply(new MyBooksResponse {
      UserId = message.UserId,
      Timestamp = DateTime.Now,
      Books = user.CurrentlyReading.ToBookDtoArray()
    });
  }
}

在深入研究用于处理此消息的实际代码之前,让我们先讨论一下此代码的运行结构。

消息综述

毫无疑问,Rhino 服务总线 (hibernatingrhinos.com/open-source/rhino-service-bus) 是一种服务总线。它是基于单向队列消息交换机制的通信框架,开发灵感主要来自于 NServiceBus (nservicebus.com)。

发送到总线的消息将到达其目标队列,在目标队列处将调用消息的使用者。图 4 中的消息使用者是 MyBooksQueryConsumer。消息使用者是实现 ConsumerOf<TMsg> 的类,并且使用相应的消息实例调用 Consume 方法以处理该消息。

您可能根据 MyBooksQueryConsumer 构造函数猜测我使用控制反转 (IoC) 容器来提供消息使用者的依赖关系。对于 MyBooksQueryConsumer,那些依赖关系就是总线本身以及 NHibernate 会话。

使用消息的实际代码非常简单。您从 NHibernate 会话获得相应的用户,然后使用请求的数据回复消息发出者。

前端也有消息使用者。该使用者针对的是 MyBooksResponse:

public class MyBooksResponseConsumer : 
  ConsumerOf<MyBooksResponse> {

  private readonly ApplicationModel applicationModel;

  public MyBooksResponseConsumer(
    ApplicationModel applicationModel) {
    this.applicationModel = applicationModel;
  }

  public void Consume(MyBooksResponse message) {
    applicationModel.MyBooks.UpdateFrom(message.Books);
  }
}

这只是利用来自消息的数据更新应用程序模型。但有一件事需要注意:使用者 是在 UI 线程上调用的,而是在后台线程上调用的。但应用程序模型已绑定到 UI,因此更新模型必须 在 UI 线程上进行。UpdateFrom 方法知道这一点,因此它将切换到 UI 线程上,以便在正确的线程中更新应用程序模型。

在后端和前端,处理其他消息的代码都是相似的。这种通信是纯粹的异步通信。您在任何时候都不需要等待来自后端的回复,也无需使用 .NET Framework 的异步 API。相反,您要使用显式的消息交换,这种交换通常是几乎立即发生的,但如果您处于连接断开模式,也可以延迟一段时间。

早先当我向后端发送查询时,我只是告诉总线发送消息,但并没有说要发送到哪里。在图 4 中,我调用了 Reply,还是没有指定应该将消息发送到哪里。那么总线是如何知道要向哪里发送这些消息的呢?

如果是向后端发送消息,那么这个问题的答案就是:配置。在 App.config 中,您会发现以下配置:

<messages>
  <add name="Alexandria.Messages"
    endpoint="rhino.queues://localhost:51231/alexandria_backend"/>
</messages>

这告诉总线所有命名空间以 Alexandria.Messages 开头的消息都应该发送到 alexandria_backend 端点。

在后端系统处理消息时,调用 Reply 表示将消息发回到其发送者。

此配置指定了消息的所有权,即消息放置到总线上后应该发送给谁,以及将订阅请求发送到哪里,才能在发布此类型的消息时将您加入到分发列表中。我在 Alexandria 应用程序中没有使用消息发布,因此本文不会涉及此话题。

会话管理

现在您已经了解通信机制如何运作,但是在继续之前还需要了解一些有关基础结构的事项。因为在所有 NHibernate 应用程序中,您都需要找到管理会话生命周期并正确处理事务的方法。

Web 应用程序的标准方法是为每个请求创建一个会话,使每个请求都有自己的会话。对于消息传送应用程序,做法几乎是一样的。不一样的地方在于不是每个请求一个会话,而是每批消息一个会话。

事实证明,这件事几乎完全是由基础结构处理的。图 5 显示了后端系统的初始化代码。

图 5 初始化消息传送会话

public class AlexandriaBootStrapper : 
  AbstractBootStrapper {

  public AlexandriaBootStrapper() {
    NHibernateProfiler.Initialize();
  }

  protected override void ConfigureContainer() {
    var cfg = new Configuration()
      .Configure("nhibernate.config");
    var sessionFactory = cfg.BuildSessionFactory();

    container.Kernel.AddFacility(
      "factory", new FactorySupportFacility());

    container.Register(
      Component.For<ISessionFactory>()
        .Instance(sessionFactory),
      Component.For<IMessageModule>()
        .ImplementedBy<NHibernateMessageModule>(),
      Component.For<ISession>()
        .UsingFactoryMethod(() => 
          NHibernateMessageModule.CurrentSession)
        .LifeStyle.Is(LifestyleType.Transient));

    base.ConfigureContainer();
  }
}

引导是 Rhino 服务总线中的显式概念,由从 AbstractBootStrapper 派生的类实现。引导程序执行的功能与一般 Web 应用程序中的 Global.asax 相同。在图 5 中,我首先构建了 NHibernate 会话工厂,然后设置容器 (Castle Windsor) 以便从 NHibenrateMessageModule 提供 NHibernate 会话。

消息模块与 Web 应用程序中的 HTTP 模块具有相同的目的:处理所有请求的共同事项。我使用 NHibernateMessageModule 管理会话生存期,如图 6 所示。

图 6 管理会话生存期

public class NHibernateMessageModule : IMessageModule {
  private readonly ISessionFactory sessionFactory;
  [ThreadStatic]
  private static ISession currentSession;

  public static ISession CurrentSession {
    get { return currentSession; }
  }

  public NHibernateMessageModule(
    ISessionFactory sessionFactory) {

    this.sessionFactory = sessionFactory;
  }

  public void Init(ITransport transport, 
    IServiceBus serviceBus) {

    transport.MessageArrived += TransportOnMessageArrived;
    transport.MessageProcessingCompleted 
      += TransportOnMessageProcessingCompleted;
  }

  private static void 
    TransportOnMessageProcessingCompleted(
    CurrentMessageInformation currentMessageInformation, 
    Exception exception) {

    if (currentSession != null)
        currentSession.Dispose();
    currentSession = null;
  }

  private bool TransportOnMessageArrived(
    CurrentMessageInformation currentMessageInformation) {

    if (currentSession == null)
        currentSession = sessionFactory.OpenSession();
    return false;
  }
}

这段代码相当简单:注册相应的事件、在合适的位置创建和释放会话,就这么简单。

这种方法的一个有趣之处在于一个批次中的所有消息将共享同一个会话,这意味着在很多情况下,您可以利用 NHibernate 的一级缓存。

事务管理

会话管理就是这样了,那么事务管理又是怎样的呢?

一项有关 NHibernate 的最佳实践就是与数据库进行的所有交互都应该通过事务进行处理。但我在这里并没有使用 NHibernate 的事务。为什么呢?

因为事务是由 Rhino 服务总线处理的。Rhino 服务总线没有让每个使用者管理自己的事务,而是采取了另一种方式。即,使用 System.Transactions.TransactionScope 创建一个事务,该事务包含该批次中消息的所有使用者。

这意味着在响应消息批次(而不是一条消息)时采取的所有操作都是同一事务的组成部分。NHibernate 将自动在环境事务中登记一个会话,当您使用 Rhino 服务总线时,您就不必再显式处理事务了。

单个会话和单个事务的组合使得将多个操作组合到单个事务单元中变得极其轻松。同时也意味着您可以直接得益于 NHibernate 的一级缓存。例如,下面是用于处理 MyQueueQuery 的相关代码:

public void Consume(MyQueueQuery message) {
  var user = session.Get<User>(message.UserId);

  Console.WriteLine("{0}'s has {1} books queued for reading",
    user.Name, user.Queue.Count);

  bus.Reply(new MyQueueResponse {
    UserId = message.UserId,
    Timestamp = DateTime.Now,
    Queue = user.Queue.ToBookDtoArray()
  });
}

处理 MyQueueQuery 和 MyBooksQuery 的实际代码几乎完全相同。那么,以下代码在性能方面对于每个会话一个事务有何意义呢?

bus.Send(
  new MyBooksQuery {
    UserId = userId
  },
  new MyQueueQuery {
    UserId = userId
  });

初看起来,这段代码要使用四个查询来收集所有必需的信息。在 MyBookQuery 中,一个查询用于获得相应的用户,另一个查询用于加载该用户的图书。在 MyQueueQuery 中看起来也是一样的:一个查询用于获取用户,另一个查询用于加载该用户的队列。

但是为整个批次使用一个会话表明您实际上在使用一级缓存,以避免不必要的查询,正如图 7 中 NHibernate Profiler (nhprof.com) 的输出所示。


图 7 处理请求的 NHibnerate Profiler 视图

支持偶尔连接的方案

实际上,应用程序在无法连接后端服务器时不会引发错误,但这样不是非常好用。

该应用程序进化的下一步是引入缓存,使应用程序在后端服务器不响应的情况下也能继续运行,从而将其变为真正的偶尔连接的客户端。但我不会使用传统的缓存体系结构,因为在这种体系结构中,应用程序代码会显式调用缓存。相反,我将在基础结构级别应用缓存。

图 8 显示了如果缓存作为消息传送基础结构的一部分,当您发送一条消息来请求有关用户图书的数据时,各项操作的顺序。

图 8 在并发消息传送操作中使用缓存

客户端发送一条 MyBooksQuery 消息。该消息被发送到总线,与此同时,查询缓存以查看其中是否有对该请求的响应。如果缓存包含对前一个请求的响应,则缓存立即让缓存的消息被使用,就像该消息是刚到达总线一样。

来自后端系统的响应到达。该消息正常地被使用,也放在缓存中。此方法表面上看似复杂,但却能带来有效的缓存行为,而且能让您在应用程序代码中几乎完全忽略缓存事项。使用永久性缓存(即使应用程序重新启动也存在的缓存),您无需从后端服务器获取任何数据,即可完全独立地操作应用程序。

现在让我们来实现此功能。我使用永久性缓存(示例代码提供了简单的实现方法,即,使用二进制序列将值保存到磁盘)并定义了以下约定:

  • 如果消息是请求/响应消息交换的一部分,则可以缓存该消息。
  • 请求和响应消息都附带用于消息交换的缓存键。

消息交换由 ICacheableQuery 接口定义,该接口具有一个 Key 属性和一个 ICacheableResponse 接口,而 ICacheableResponse 接口具有 Key 和 Timestamp 属性。

为了实现此约定,我编写要在前端运行的 CachingMessageModule,用于截取传入和传出的消息。图 9 显示了如何处理传入的消息。

图 9 缓存传入的连接

private bool TransportOnMessageArrived(
  CurrentMessageInformation
  currentMessageInformation) {

  var cachableResponse = 
    currentMessageInformation.Message as 
    ICacheableResponse;
  if (cachableResponse == null)
    return false;

  var alreadyInCache = cache.Get(cachableResponse.Key);
  if (alreadyInCache == null || 
    alreadyInCache.Timestamp < 
    cachableResponse.Timestamp) {

    cache.Put(cachableResponse.Key, 
      cachableResponse.Timestamp, cachableResponse);
  }
  return false;
}

到这里就没有太多内容了 - 如果消息是可缓存的响应,我就将其放到缓存中。但还有一件事需要注意:我对无序的消息(即具有较早时间戳的消息比具有较晚时间戳的消息晚到达)进行了处理。这可以确保只将最新的信息保存到缓存中。

您可以从图 10 看到,处理传出的消息和分派缓存中的消息更加有趣。

图 10 分派消息

private void TransportOnMessageSent(
  CurrentMessageInformation 
  currentMessageInformation) {

  var cacheableQuerys = 
    currentMessageInformation.AllMessages.OfType<
    ICacheableQuery>();
  var responses =
    from msg in cacheableQuerys
    let response = cache.Get(msg.Key)
    where response != null
    select response.Value;

  var array = responses.ToArray();
  if (array.Length == 0)
    return;
  bus.ConsumeMessages(array);
}

我从缓存中收集了缓存的响应,并对它们调用 ConsumeMessages。这导致总线调用通常的消息调用逻辑,因此看起来很像消息又一次到达。

但请注意,即使存在缓存的响应,您仍要发送消息。原因在于这样您就可以向用户提供快速(缓存的)响应,并在后端回复新消息时更新向用户显示的信息。

后续步骤

我已经介绍了智能客户端应用程序的基本构造块:如何构造后端,以及如何构造智能客户端应用程序和后端之间的通信模式。后者很重要,因为选错通信模式可能会导致分布式计算的各种谬论。我还涉及了批处理和缓存,它们是提升智能客户端应用程序性能的两种重要方法。

您已经了解如何在后端管理事务和 NHibernate 会话,如何使用和回复来自客户端的消息,以及所有内容如何融入引导程序。

在本文中,我的主要关注点在基础结构方面;下一期的文章将介绍在后端和智能客户端应用程序之间传送数据的最佳实践,以及分布式更改管理的模式。

Oren Eini*(笔名 Ayende Rahien)是多个开源项目(其中包括 NHibernate 和 Castle)的活跃成员,也是很多其他工具(包括 Rhino Mocks、NHibernate Query Analyzer 和 Rhino Commons)的开发者。Eini 还负责 NHibernate 的可视化调试程序 NHibernate Profiler (nhprof.com) 的开发。您可以访问 Eini 的博客 ayende.com/blog。*