MassTransit-概念-02消息

消息

在 MassTransit 中,消息契约是通过代码优先的方式创建.NET 类型来定义的。消息可以用记录(record)、类(class)或接口(interface)来定义。消息应该只包含属性,不应该包含方法和其他行为。

MassTransit 使用包括命名空间的完整类型名称来定义消息契约。当在两个独立的项目中创建相同的消息类型时,命名空间必须匹配,否则消息将不会被消费.

下面的消息示例展示了使用每种支持的契约类型来更新客户地址的相同命令。

消息类型

消息必须是引用类型,并且可以使用记录(records)、接口(interfaces)或类(classes)来定义。

记录(Records)

namespace Company.Application.Contracts
{
    using System;

    public record UpdateCustomerAddress
    {
        public Guid CommandId { get; init; }
        public DateTime Timestamp { get; init; }
        public string CustomerId { get; init; }
        public string HouseNumber { get; init; }
        public string Street { get; init; }
        public string City { get; init; }
        public string State { get; init; }
        public string PostalCode { get; init; }
    }
}

接口(Interfaces)

namespace Company.Application.Contracts
{
    using System;

    public interface UpdateCustomerAddress
    {
        Guid CommandId { get; }
        DateTime Timestamp { get; }
        string CustomerId { get; }
        string HouseNumber { get; }
        string Street { get; }
        string City { get; }
        string State { get; }
        string PostalCode { get; }
    }
}

当使用接口定义消息类型时,MassTransit 将创建一个动态类来实现该接口以进行序列化,从而允许具有只读属性的接口呈现给消费者。要创建接口消息,请使用消息初始化器(message initializer.)。

类(Classes)

namespace Company.Application.Contracts
{
    using System;

    public class UpdateCustomerAddress
    {
        public Guid CommandId { get; set; }
        public DateTime Timestamp { get; set; }
        public string CustomerId { get; set; }
        public string HouseNumber { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string PostalCode { get; set; }
    }
}

不推荐使用带有 private set; 的属性,因为默认情况下使用 System.Text.Json 时它们不会被序列化。

当工程师刚开始接触消息传递时,一个常见的错误是创建一个消息的基类,并尝试在消费者中分派该基类——包括子类的行为。这总是会导致痛苦和折磨,所以对基类说“不”吧。

消息属性

属性 (Attribute)描述 (Description)
EntityName交换机或主题名称
ExcludeFromTopology除非直接消费或发布,否则不要创建交换机或主题
ExcludeFromImplementedTypes不要为消息类型创建中间件过滤器
MessageUrn消息urn

消息名称

有两种主要的消息类型:事件(events)和命令(commands)。在为消息选择名称时,消息的类型应决定消息的时态。

命令(Commands)

命令(Commands)告诉服务执行某些操作,通常一个命令应该只被一个消费者消费。如果有一个命令,例如 SubmitOrder,那么应该只有一个消费者实现 IConsumer<SubmitOrder> 或一个带有 Event<SubmitOrder> 配置的 saga 状态机。通过保持命令与消费者之间的一对一关系,命令可以被发布(published)1,并且它们将自动路由到消费者。

在使用 RabbitMQ 时,这种方法没有额外的开销。而Azure Service Bus 和 Amazon SQS 具有更复杂的路由结构,由于这种结构,可能会产生额外的费用,因为消息需要从主题转发到队列。对于低到中等消息负载,这并不是一个大问题,但对于较大的高负载,直接发送(使用 Send)命令到队列以减少延迟和成本可能更为可取。

命令应遵循动词-名词序列,采用“告诉”风格。例如:

  • UpdateCustomerAddress
  • UpgradeCustomerAccount
  • SubmitOrder

事件(Events)

事件表示某件事情已经发生。事件通过 ConsumeContext(在消息消费者内部)、IPublishEndpoint(在容器作用域内)或 IBus(独立使用)发布。

事件应遵循名词-动词(过去时态)序列,表示某件事情已经发生。一些示例事件名称可能包括:

  • CustomerAddressUpdated
  • CustomerAccountUpgraded
  • OrderSubmitted, OrderAccepted, OrderRejected, OrderShipped

消息头(Message Headers)

MassTransit 将每个发送或发布的消息封装在一个消息信封中(由信封包装模式:Envelope Wrapper partten描述)。信封添加了一系列消息头,包括:

属性 (Property)类型 (Type)描述 (Description)
MessageId自动 (Auto)为每个消息生成,使用 NewId.NextGuid
CorrelationId用户 (User)由应用程序分配,或根据约定自动分配,应唯一标识操作、事件等。
RequestId请求 (Request)由请求客户端分配,并由 Respond 方法自动复制,以将响应与原始请求关联。
InitiatorId自动 (Auto)当从消费者、saga 或活动发布或发送时分配,值为所消费消息的 CorrelationId
ConversationId自动 (Auto)当第一个消息发送或发布且没有可用的消费消息时分配,确保同一对话中的一组消息具有相同的标识符。
SourceAddress自动 (Auto)消息的来源地址(对于从 IBus 发布或发送的消息,可能是临时地址)。
DestinationAddress自动 (Auto)消息发送到的地址。
ResponseAddress请求 (Request)响应请求应发送到的地址。如果不存在,响应将被发布。
FaultAddress用户 (User)消费者故障应发送到的地址。如果不存在,故障将被发布。
ExpirationTime用户 (User)消息应过期的时间,传输可能会在过期时间之前移除未被消费的消息。
SentTime自动 (Auto)消息发送的时间,UTC 时间。
MessageType自动 (Auto)消息类型的数组,采用 MessageUrn 格式,可以反序列化。
Host自动 (Auto)发送或发布消息的机器的主机信息。
Headers用户 (User)其他头信息,可以由用户、中间件或诊断跟踪过滤器添加。

消息头可以通过 ConsumeContext 接口读取,并使用 SendContext 接口指定。

消息关联(Message Correlation)

消息通常是对话的一部分,标识符用于将消息连接到该对话。在前面的部分中,MassTransit 支持的头信息,包括 ConversationIdCorrelationIdInitiatorId,用于将单独的消息组合成对话。由消费者发布或发送的出站消息将具有与消费消息相同的 ConversationId。如果消费消息具有 CorrelationId,该值将被复制到 InitiatorId。这些头信息捕获了对话中涉及的消息流。

在适当的情况下,开发者可以在发布或发送消息时设置 CorrelationIdCorrelationId 可以在 PublishContextSendContext 上显式设置,或者在使用消息初始化器时通过 __CorrelationId 属性设置。下面的示例展示了如何使用这些方法中的任何一种。

使用 SendContext 设置 CorrelationId

await endpoint.Send<SubmitOrder>(new { OrderId = InVar.Id }, sendContext =>
    sendContext.CorrelationId = context.Message.OrderId);

使用消息初始化器设置 CorrelationId

await endpoint.Send<SubmitOrder>(new
{
    OrderId = context.Message.OrderId,
    __CorrelationId = context.Message.OrderId
});

关联约定(Correlation Conventions)

CorrelationId 也可以通过约定设置。MassTransit 默认包含几个约定,这些约定可以用作初始化 CorrelationId 头的源。

  • 如果消息实现了 CorrelatedBy<Guid> 接口,该接口具有 Guid CorrelationId 属性,则其值将被使用。
  • 如果消息具有名为 CorrelationIdCommandIdEventId 的属性,且该属性为 GuidGuid?,则其值将被使用。
  • 如果开发者为消息类型注册了 CorrelationId 提供者,则将使用该提供者获取值。

最后一个约定要求开发者在总线创建之前注册 CorrelationId 提供者。约定可以通过两种方式注册,一种是新方法,另一种是原始方法,它只是调用新方法。下面展示了新方法以及之前方法的示例。

// 使用 OrderId 作为消息的 CorrelationId
GlobalTopology.Send.UseCorrelationId<SubmitOrder>(x => x.OrderId);

// 之前的方法,现在调用上述新方法
MessageCorrelation.UseCorrelationId<SubmitOrder>(x => x.OrderId);

约定也可以在总线配置期间指定,如下所示。在这种情况下,约定适用于配置的总线实例。之前的方法是全局配置,由所有总线实例共享。

cfg.SendTopology.UseCorrelationId<SubmitOrder>(x => x.OrderId);

注册 CorrelationId 提供者应在应用程序早期进行,在总线配置之前。一个简单的方法是将注册方法放入类方法中,并在应用程序启动期间调用它。

Saga 关联(Saga Correlation)

Saga 必须有一个 CorrelationId,它是 saga 存储库使用的主键,也是消息与特定 saga 实例关联的方式。MassTransit 遵循上述约定来获取用于创建新 saga 实例或加载现有 saga 实例的 CorrelationId。新创建的 saga 实例将从启动消息中分配 CorrelationId

标识符(Identifiers)

MassTransit 使用并高度鼓励使用 Guid 标识符。分布式系统如果使用单调递增的标识符(如 intlong),会因为锁定和递增共享计数器的瓶颈而崩溃。历史上,某些类型(好吧,我们会指出它们——SQL DBA)反对使用 Guid(或他们的术语,uniqueidentifier)作为键——特别是聚集主键。然而,对于 MassTransit,我们已经解决了这个问题。

MassTransit 使用 NewId 生成唯一、顺序且表示为 Guid 的标识符。生成的标识符对聚集索引友好,并且有序,因此 SQL Server 可以高效地将它们插入到以 uniqueidentifier 作为主键的数据库中。

要创建一个 Guid,请调用 NewId.NextGuid(),而不是 Guid.NewGuid(),并享受快速、分布式唯一标识符的好处。

指导原则(Guidance)

在定义消息契约时,以下是基于多年使用 MassTransit 的经验以及开发者对 MassTransit 新手的持续提问而得出的通用指导原则。

  • 使用记录(records),将属性定义为公共的,并指定 { get; init; } 访问器。使用构造函数/对象初始化器或消息初始化器创建消息。
  • 使用接口(interfaces),仅指定 { get; } 访问器。使用消息初始化器创建消息,并使用 Roslyn 分析器识别缺失或不兼容的属性。
  • 限制继承的使用,注意多态消息路由。如果需要深入研究消息路由以解决某个问题,包含十几个接口的消息类型可能会让人感到困扰。
  • 类继承与接口有相同的指导原则,但需要更加谨慎。
  • 消息设计不是面向对象设计。消息应包含状态,而不是行为。行为应在单独的类或服务中。
  • 消费基类类型并期望多态方法行为几乎总是会导致问题
  • 一个庞大的基类可能会在未来的变化中带来痛苦,特别是在支持多个消息版本时。

消息继承(Message Inheritance)

消息设计不是面向对象设计。

这个概念经常出现,以至于它值得拥有自己的专门部分。根据设计,MassTransit 将您的类、记录和接口视为“契约”。

举个例子,假设你有一个由以下 dotnet 类定义的消息:

public record SubmitOrder
{
    public string Sku { get; init; }
    public int Quantity { get; init; }
}

你希望你所有的消息都有一个共同的属性集,所以你尝试这样做。

public record CoreEvent
{
    public string User { get; init; }
}

public record SubmitOrder : 
    CoreEvent
{
    public string Sku { get; init; }
    public int Quantity { get; init; }
}

如果你尝试消费一个 Batch<CoreEvent> 并期望得到各种类型,其中一种是 SubmitOrder。在面向对象编程的世界里,这完全合乎逻辑,但在 MassTransit 契约设计中则不然。应用程序已经声明它关心的是 CoreEvent 的批次,所以它只会得到单个属性 User。这不是使用 System.Text.Json 的症状,这是 MassTransit 自第一天以来的标准行为,即使在使用 Netwonsoft.Json 时也是如此。MassTransit 将始终尊重已设计的契约。

如果你希望有一个可用的标准属性集,当然可以使用基类,或者将它们打包成一个单一属性,这是我们的偏好。如果你想要订阅类的所有实现,那么你需要订阅类的所有实现。

原文地址:[https://masstransit.io/documentation/concepts/messages]:https://masstransit.io/documentation/concepts/messages


  1. 通常情况下,Commands使用Send,Events使用Publish发布; ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值