使用 MassTransit 的请求-响应消息模式

62 阅读5分钟

构建分布式应用乍一看可能很简单。不过是服务器之间相互通信嘛。对吧?

然而,它带来了一系列你必须考虑的潜在问题。如果网络出现故障怎么办?服务意外崩溃怎么办?你尝试扩展,结果一切在负载下崩溃怎么办?这就是你的分布式系统通信方式变得至关重要的地方。

传统的同步通信,即服务之间直接相互调用,本质上是非常脆弱的。它造成了紧密耦合,使得整个应用容易受到单点故障的影响。

为了应对这一问题,我们可以转向分布式消息传递(并引入一整套不同的问题,但这又是另一个话题了)。

在 .NET 领域中实现这一目标的一个强大工具是 MassTransit。

今天,我们将探讨 MassTransit 对请求-响应模式的实现。

请求-响应消息模式简介

让我们首先解释一下请求-响应消息模式是如何工作的。

请求-响应模式就像进行传统的函数调用,但通过网络实现。一个服务,即请求者,发送一个请求消息并等待相应的响应消息。从请求者的角度来看,这是一种同步通信方式。

优点

  • 松散耦合:服务之间不需要直接了解对方,只需了解消息契约。这使得变更和扩展变得更加容易。
  • 位置透明性:请求者无需知道响应者的位置,从而提高了灵活性。

缺点

  • 延迟: 消息传递的开销增加了一些额外的延迟。
  • 复杂性:引入消息系统和管理额外的基础设施可能会增加项目复杂性。

使用 MassTransit 的请求-响应消息模式

MassTransit 默认支持请求-响应消息模式。我们可以使用请求客户端发送请求并等待响应。请求客户端是异步的,并支持 await 关键字。请求还将默认具有 30 秒的超时时间,以防止等待响应过久。

让我们设想一个场景,你有一个订单处理系统,需要获取订单的最新状态。我们可以从一个订单管理服务中获取状态。使用 MassTransit,你将创建一个请求客户端来启动该过程。这个客户端将发送一个 GetOrderStatusRequest 消息到总线。

public record GetOrderStatusRequest
{
    public string OrderId { get; init; }
}

在订单管理端,一个响应者(或消费者)将监听 GetOrderStatusRequest 消息。它接收请求,可能查询数据库以获取状态,然后向总线发送一个 GetOrderStatusResponse 消息。原始请求客户端将等待此响应,并据此进行相应处理。

public class GetOrderStatusRequestConsumer : IConsumer<GetOrderStatusRequest>
{
    public async Task Consume(ConsumeContext<GetOrderStatusRequest> context)
    {
        // 从数据库中获取订单状态

        await context.ResponseAsync<GetOrderStatusResponse>(new
        {
            // 设置响应属性
        });
    }
}

在模块化单体中获取用户权限

这里有一个真实场景,我的团队决定实施这种模式。我们正在构建一个模块化单体,其中一个模块负责管理用户权限。其他模块可以调用用户模块来获取用户的权限。在我们仍然处于单体系统内部时,这种方式运行得很好。

然而,在某个时刻,我们需要将一个模块提取到一个独立的服务中。这意味着通过简单方法调用与用户模块的通信将不再有效。

幸运的是,我们已经在系统中使用 MassTransit 和 RabbitMQ 进行消息传递。

所以,我们决定使用 MassTransit 的请求-响应功能来实现这一点。

新服务将注入一个 IRequestClient 。我们可以使用它来发送一个 GetUserPermissions 消息并等待响应。

MassTransit 的一个非常强大的特性是,你可以等待多个响应消息。在这个例子中,我们正在等待一个 PermissionsResponseError 响应。这很棒,因为我们也有一方法在消费者中处理失败情况。

internal sealed class PermissionService(
    IRequestClient<GetUserPermissions> client)
    : IPermissionService
{
    public async Task<Result<PermissionsResponse>> GetUserPermissionsAsync(
        string identityId)
    {
        var request = new GetUserPermissions(identityId);

        Response<PermissionsResponse, Error> response =
            await client.GetResponse<PermissionsResponse, Error>(request);

        if (response.Is(out Response<Error> errorResponse))
        {
            return Result.Failure<PermissionsResponse>(errorResponse.Message);
        }

        if (response.Is(out Response<PermissionsResponse> permissionResponse))
        {
            return permissionResponse.Message;
        }

        return Result.Failure<PermissionsResponse>(NotFound);
    }
}

在用户模块中,我们可以轻松实现 GetUserPermissionsConsumer 。如果找到权限,它将返回 PermissionsResponse ;如果失败,则返回 Error

public sealed class GetUserPermissionsConsumer(
    IPermissionService permissionService)
    : IConsumer<GetUserPermissions>
{
    public async Task Consume(ConsumeContext<GetUserPermissions> context)
    {
        Result<PermissionsResponse> result =
            await permissionService.GetUserPermissionsAsync(
                context.Message.IdentityId);

        if (result.IsSuccess)
        {
            await context.RespondAsync(result.Value);
        }
        else
        {
            await context.RespondAsync(result.Error);
        }
    }
}

结束语

通过采用 MassTransit 的消息模式,你正在构建一个更加稳固的基础。你的 .NET 服务现在耦合度更低,这为你提供了灵活性,可以独立地演进它们,并应对那些不可避免的网络故障或服务中断。

请求-响应模式是你消息传递工具库中的强大工具。MassTransit 使其实现变得非常简单,确保请求和响应能够可靠地传递。

我们可以使用请求-响应模式来实现模块化单体中各模块之间的通信。然而,不要将其推向极端,否则你的系统可能会因延迟增加而受到影响。

从小处着手,进行实验,看看消息传递的可靠性和灵活性如何改变你的开发体验。

今天就到这里。期待与你再次相见。

关注微信公众号【.NET微服务砖厂】从单体到分布式,用 .NET 码好每一块服务砖。

qrcode_for_gh_8757e0116df4_344.jpg