NServiceBus入门:发布事件(Introduction to NServiceBus: Publishing events)

原文地址:https://docs.particular.net/tutorials/intro-to-nservicebus/4-publishing-events/

侵删。

 

这个教程到目前为止,我们都是在发送command——单向的从发送者到接受者message。还有其他的message类型我们即将去关注,那就是event。在很多方面,event和command很像。他们都是简单的类,因此你可以用很相似的方法来处理它们。但是从一个架构角度来说,command和event是两个极端。我们可以使用event的优点来为我们软件设计增加更多的可能。

在接下来的25-30分钟里面,你会学习如何使用发布者订阅者模式来创造更加容易维护的代码。同时,我们将会学习如何定义,发布和订阅event。

事件

event是另外一种message类型。它能够发布给多个接收者,而command只能发送给一个接收者。然我们看看标准的command和event的定义:

command是一种message类型,它可以从一个或者多个发送者发送到一个接收者处理。

event是一种message类型,它由一个发送者发布,可以被多个(潜在的)接收者处理。

你可以看到在很多方面,command和event都完全相反,并且他们定义上面的不同能够让他们被使用在不同的场景中。

command能够被从任何地方发送过来,但是它只能由一个接收者处理。这个和所有的互联网服务,或者其他的RPC类型的服务一样。然而与之很大的不同之处是单向的message并不会想互联网服务那样包含返回值。这就意味着command的handler会为任何调用它的端提供服务,并且发送者完全意识到它发送这个command之后会发生一些什么事情。发送者会声明类似“请你为我做些什么吗。”因此command应该以一种祈使语气的方式来命名,例如PlaceOrder和ChargeCreditCard。这会在发送者和接收者之间产生紧密的耦合,这样即使command被拒绝了,就算其他人告诉你应该怎么做你也无法做到,因为发送者无法自己搞定。

而一个event,恰恰相反,只能由一个逻辑发送者发送,但是可以被多个,或者一个接收者接收,甚至没有接收者。它相当于发布一个“某件事情已经发生了”的声明。一个订阅者是不能拒绝或者取消一个事件的,就像你不能阻止纽约时报把报纸邮递给它的所有订阅者。发布者并不知道订阅者会选择一个怎么样的方式去处理这个事件,它只是做一个声明。因此,事件应该以过去时来命名,一般会以-ed结尾,就像OrderPlaced这样CreditCardCharged。这种是一种松耦合的方式,因为当协议(message的内容)必须被双方接受,却并没有强制要求事件的接收者要做一些什么事情。

让我们一个一个地去比较二者之间的不同:

  command event
标记接口 ICommand IEvent
逻辑发送者 多个 一个
逻辑接收者 一个 多个(或者一个)
目的 请做一些事情 有件事情发生了
命名方式 祈使 过去式
例子

PlaceOrder
ChargeCreditCard

OrderPlaced
CreditCardCharged

耦合性

从这个比较中我们可以看到,很明显command和event在有些时候会组合起来。command可能会从一个网站页面UI上面到达后台,发布一个dosomething 的 command。系统进行这个工作,会发布一个something happened的event,系统中的其他组件就会获得这个event然后做出反应。

更多细节,参阅Messages, Events and Commands

通过发布事件的方式,我们能够让整个系统的部件更加松耦合,也可以为我们提供一个更加灵活的方式去设计一个维护性更强的系统。

通过解耦来使代码更健壮

设想一下你正在实现一个电商网站的SubmitOrder 方法。想要完成这个sale过程,你可能需要获得购物车里面的东西,插入一个订单和订单信息到数据库中,申请一个信用卡交易然后获取这个授权,最后把收据信息用电子邮件发送给客户。

你可以用一个几百行代码的类来完成这些工作。你可以把每个任务都封装成一个方法,然后在SubmitOrder 方法中一个个地去调用他们,这样做虽然能够让每个方法更加容易管理,但是它还是不能减少整个过程中出现的风险。

当这整个过程的一个步骤出问题之后,就会留下一个需要手动去调整的不完整过程,可能要手动去数据库里面修改数据,或者手动协协调信用卡的支付的处理,或者手动发送确定邮件给客户。

通过使用event,我们可以更好地遵从single responsibility principle (单一职责原则)然后把这些关注点分成几个message handler。只要简单地发布一个OrderPlaced事件,所有其他订阅这个事件的组件将会完成他们自己职责内的工作。

这就意味着,如果信用卡处理的过程改变了,我们不需要动到任何系统中除了直接和信用卡直接关联的代码之外的代码。

定义event

创建一个event message和创建一个command类似。我们只要创建一个类,然后实现IEvent(而不是ICommand)标记接口。

public class SomethingHappened :
    IEvent
{
    public string SomeProperty { get; set; }
}

所有其他的配置都和command差不多。属性可以是普通类,复杂类或者集合——只要message的序列化器能够支持就可以了。

对于event,你应该更加注意精简,不要在一个event message中添加太多的信息。对于command,某些时候一些复杂的东西可能无法避免,因为command的接收者需要这些信息来完成它的工作。适当的复杂度对于command来说是可以接受的,因为command的发送者和接收者高度耦合。但是对于event来说,情况就不一样了。因为event的发布者并不知道(也不关心)它有多少个订阅者,如果一个事件的内容需要改变,是很难再订阅者那里做出改变的。

处理event

创建一个handler类,实现IHandleMessages<T>接口,其中的T是event message的类型。

public class SomethingHappenedHandler :
    IHandleMessages<SomethingHappened>
{
    public Task Handle(SomethingHappened message, IMessageHandlerContext context)
    {
        // Do something with the event here

        return Task.CompletedTask;
    }
}

订阅event

对于MSMQ transport,NServiceBus 需要知道那个endpoint是用来发布事件的,因此它可以给发布者endpoint发送订阅的请求message。

你可以通过使用路由API来配置发布者endpoint,就像这样:

var transport = endpointConfiguration.UseTransport<MsmqTransport>();

var routing = transport.Routing();
routing.RegisterPublisher(typeof(SomethingHappened), "PublisherEndpoint");

一些其他的transport有一些内置的发布者订阅者功能,所以要订阅一个event就只要为它创建一个message handler就可以了,不需要订阅请求的message了。这是因为路由配置仅仅针对transport,不需要配置发布者的transport一般不包含这种API。

练习

既然我们已经学习了关于event和发布者订阅者模式的相关知识,让我们来让这些知识为构建我们的系统做出贡献。当一个用户下了一个订单的时候,我们将会发布一个OrderPlaced event,然后把它们发送到两个新的endpoint去:Billing 和 Shipping。

我们将会创建一个新的OrderBilled event,一旦信用业务完成后,这个event会被Billing endpoint发布。

image

当Shipping endpoint 接收到OrderPlaced 和 OrderBilled事件的时候,它会知道现在是时候把产品寄给客户了。因为这需要保存状态,我们不能仅仅靠message handler完成这个任务。要完成这个功能,我们需要一个Saga, 但是这个概念不包含在这个课程中。

创建一个事件

让我们创建一个叫做OrderPlaced的事件:

1.在Message项目中,创建一个新的叫做OrderPlaced的类。

2.将OrderPlaced标记成public,实现IEvent接口。

3.添加一个公共的属性,OrderId。

这些完成之后,你的OrderPlaced类应该是这样子的:

public class OrderPlaced :
    IEvent
{
    public string OrderId { get; set; }
}

发布一个事件

现在OrderPlaced event已经被定义了,我们可以使用PlaceOrderHandler发布它。

1.在Sales endpoint中找到PlaceOrderHandler

2.把Task.CompletedTask;去掉

3.将这个handler方法修改成这样:

public Task Handle(PlaceOrder message, IMessageHandlerContext context)
{
    log.Info($"Received PlaceOrder, OrderId = {message.OrderId}");

    // This is normally where some business logic would occur

    var orderPlaced = new OrderPlaced
    {
        OrderId = message.OrderId
    };
    return context.Publish(orderPlaced);
}

如果我们现在运行这个解决方案,看上去不会有什么新的东西发生。我们发布了一个message,但是没有人订阅它,因此没有任何真实的message发送到任何地方。就像一份报纸没有流通一样。要解决这个问题,我们需要一个订阅者。

创建一个订阅者

和PlaceOrder command不一样,OrderPlaced 声明了一个事件已经发生了。所以其他的endpoint应该做出下完订单之后的响应。

当订单下完之后,我们要从信用卡中扣款。所以我们创建了一个Billing service,这个service会订阅OrderPlaced event来处理付款的业务。

因为这个是我们创建的第三个endpoint了,所以我们相对简单地说一下创建过程。你也可以回去第二节课看看Sales endpoint的建立过程来找到更加详细的创建过程。

1.创建一个新的控制台应用程序Billing

2.从Sales中的Program.cs 文件里面把配置拷贝到Billing的Program.cs 中去。

3.在Billing endpoint的 Program.cs,改变控制台的名称成“Billing”。

4.在Billing endpoint中,添加OrderPlacedHandler类,把它标记成Public,然后实现<OrderPlaced>.接口。

修改handler类来记录event的接收情况:

最后,修改解决方案的属性,让它运行的时候能够启动Billing。

public class OrderPlacedHandler :
    IHandleMessages<OrderPlaced>
{
    static ILog log = LogManager.GetLogger<OrderPlacedHandler>();

    public Task Handle(OrderPlaced message, IMessageHandlerContext context)
    {
        log.Info($"Received OrderPlaced, OrderId = {message.OrderId} - Charging credit card...");

        return Task.CompletedTask;
    }
}

订阅一个event

我们现在有一个OrderPlaced的handler了,但是就像我们生活中一样,仅仅拥有一个邮箱并不会让message自动发到你的房子里去。我们需要让发布者知道我们想要订阅message。

在Billing endpoint中,找到Program.cs 里面的AsyncMain 方法。使用transport 变量来获得路由配置然后配置OrderPlaced的发布者信息:

var routing = transport.Routing();
routing.RegisterPublisher(typeof(OrderPlaced), "Sales");

现在当我们运行解决方案的时候,我们会看到在Billing窗口中出现下面这些信息:

INFO  Billing.OrderPlacedHandler Received OrderPlaced, OrderId = 01698293-9da9-4606-8468-2b7f1b86b380 - Charging credit card...

这简直太棒了,但是我们不能就此止步。发布者订阅者模式的目的就是我们可以有多个订阅者。

创建另一个订阅者

在真正的系统中,在一个下完一个订单并且支付完毕之后,我们需要将产品邮寄给用户。所以让我们在添加一个event和两个订阅者。一旦信用卡中的钱扣掉之后,我们会发布一个OrderBilled event。我们将会创建一个新的Shipping endpoint订阅这两个event。

这也是一个很好的检验自己的机会。如果你能独自完成这些步骤,你就可以确定你已经对我们目前教的东西有一个很好的了解了。

1.在Messages中,新建一个新的叫做OrderBilled的event,实现IEvent接口,添加一个属性OrderId。

2.在Billing中,在OrderPlacedHandler结尾处发布orderBilled event。

3.新建一个新的endpoint,叫做Shipping。把控制台的endpoint的名字都设置成shipping,然后让它在debug开始的时候运行。

4.在Shipping中,为OrderPlaced创建一个message handler。

5.在Shipping中,为OrderBilled创建一个message handler。

6.配置Shipping,让它订阅Sales发布的OrderPlaced event。(你可能在把endpoint配置从Billing拷贝过来的时候已经一起把这个配置拷贝了)

7.配置Shipping,让它订阅Billing发布的OrderBilled event。

运行解决方案

如果一切顺利的话,你会在Shipping窗口中看到下面的东西。

INFO  Shipping.OrderPlacedHandler Received OrderPlaced, OrderId = 96ee660a-5dd7-4772-9058-863d303ee0aa - Should we ship now?
INFO  Shipping.OrderBilledHandler Received OrderBilled, OrderId = 96ee660a-5dd7-4772-9058-863d303ee0aa - Should we ship now?

 

当然。这些信息可能不会按照这个顺序显示出来。因为异步的messaging不能保证message的排序。尽管OrderBilled理论上是在OrderPlaced之后法伤,但是仍然有可能OrderBilled先到达。

你要注意,在同一个解决方案中,每个handler的message都在说“我们可以发货了吗?”这是因为message handler都是无状态的。就像HTTP请求一样,message handler并不知道在这之前发生什么。NServiceBus 包含了一个叫做Saga的特性来提供这种保存两个message状态的功能,但是我们不会再这节课里面谈论这个。

总结

在这节课中,我们已经学会了所有关于event的知识,它与command是怎样的不同,它是如何让我们的系统更加低耦合更符合单一职责原则。我们从Sales endpoint发布了OrderPlaced event,然后创建了Billing和Shipping endpoint来订阅这个事件。

我们还从Billing endpoint中发布了OrderBilled event,然后在Shipping endpoint中订阅它。

在这个教程的最后一节课中,我们会看到当引入错误到我们系统的时候,会发生什么事情,然后我们会如何自动重试来创建一个弹性系统。

posted @ 2017-03-19 17:57  balavatasky  阅读(451)  评论(0编辑  收藏  举报