PHP 中的松散耦合微服务

2022年 10月 12日 40.8k 0

PHP 中的松散耦合微服务

在本文中,我们将深入探讨在 PHP 中集成微服务并保持它们松散耦合的主题。

我们将专注于通过 集成messaging,因为通过HTTP 集成微服务有很多缺点,需要单独的文章来解决。

除了如何实现这一点的细节和理论之外,我们将学习如何在 PHP 中使用Ecotone 框架(与Symfony和一起使用Laravel)实际实现它。

共享消息类

因此,PHP 中最常用的解决方案之一是在单独的包中共享类,或者只是将它们复制到每个服务中。

这暗示了我们正在处理的内容并有助于反序列化。然而,我们共享的类越多,我们需要做的改变就越多。

然后是螺旋式下降,当我们进行更改时,它不是发布单个服务,而是发布多个服务,因为我们需要使它们保持同步。

路由消息,而不是类

按类路由消息的工作原理是将类的名称保留在消息的标题中。稍后使用此标头来了解我们应该反序列化到哪个类。

当我们期望其他服务能够理解我们的类时,它会造成这样的情况,即其他服务需要有一个名称完全相同的类。

我们可以很容易地想象这样的情况,我们忘记更改其中一个服务的名称或延迟发布另一个服务,这将创建failure due to mismatch in class names.

实际上,如果我们在单个服务中处理消息,它仍然可能会产生问题。

如果消息在队列中并且我们将change class name or namespace,将无法反序列化它。

那么消息应该如何路由呢?

在大多数情况下我们已经使用了这个解决方案,但是我们还没有将它提升到应用程序级别,解决方案是routing keys.

路由键可以看作是事件和命令的应用程序级名称。

它们在服务之间创建契约和共享命名,因此我们可以理解消息背后的意图。

当消息通过路由键进行路由时,我们可以将给定的路由键映射到类名。

结果,在一项服务中重构类名将不需要更改另一项服务。

<?php
# For simplicity example of routing within single service. 
# Next examples will show integration between services.

class OrderController
{
    public function placeOrder(Request $request, CommandBus $commandBus): void
    {
       $commandBus->sendWithRouting("place_order", $request->getBody(), "application/json");
    }
}

class OrderService
{
    #[CommandHandler("place_order")]
    public function placeOrder(PlaceOrder $command): void
    {
        // place order
    }
}

使用 Ecotone,您实际上不需要构建任何类型的routing-key -> class-name地图。

消息基于给定的处理程序路由routing key,然后payload对消息进行反序列化based on first method parameter

明确说明您的公共 API

每当给定的事件或命令暴露给其他服务时,它就会成为您公共 API 的一部分。

如果您将每个事件都发布到外部世界,那么当您想要更改它时就会成为一个问题,因为不太清楚它是否会影响外部服务。

服务之间的契约是应用程序级别的一部分,它们应该是显式的,而不是隐藏在 Message Broker 实现中。

Ecotone 为发布消息的服务提供DistributedBus。

如果事件是要被其他服务消费的,我们可以这样explicit on the application level,我们需要aware, if we want to change it

<?php

class OrderService
{
    #[EventHandler]
    public function placeOrder(OrderWasPlaced $event, DistributedBus $distributedBus): void
    {
       $distributedBus->convertAndPublishEvent("order.was_placed",$event);
    }
}

在消费者方面,我们明确表示我们订阅的消息来自外部服务。

<?php

class NotificationService
{
    #[Distributed]
    #[EventHandler("order.was_placed")]
    public function sendEmail(OrderWasPlaced $event): void
    {
        // send email
    }
}

如果我们不添加Distributed到此Event Handler事件中,将被视为本地(私人)事件。

同样适用于,如果不是明确的Distributed Command Handler,任何服务都无法执行我们的。Command Handlerdistributed

这让新加入者清楚地知道,从项目的长期来看,哪些处理程序external serviceslocal ones.

为了推动这一点,您可以使用Pact添加 Consumer Driven Contract 。

这将让您知道您的消息中的哪些字段被其他服务使用,因此您可以轻松修改未使用的字段。

公共和私人活动

即使我们将明确发布分布式事件并限制其数量,我们仍然可能会被其他服务阻止以进行内部更改。

如果我们要修改payload或者我们想要删除或替换事件,那么我们最终可能会影响其他方。

这就是为什么值得区分事件publicprivate事件的原因。

通过使用DistributedBus,我们已经向前迈进了一步,明确了我们要向外部发送的事件,但是这样做还有一个好处。

在发布事件之前,我们现在可以将其转换为不同的事件。此事件将成为我们的公共 API,而另一个将在发布服务中保持私有。

这给了我们很大的力量。只要我们能够交付这个public event,我们就可以按照我们想要的方式修改我们的内部服务,而不会影响其他方。

我们可以用额外的细节丰富公共事件,所以need for consumption of other events or calling our HTTP Api will decrease.

<?php

class OrderService
{
    #[EventHandler]
    public function placeOrder(OrderWasPlaced $event, DistributedBus $distributedBus, OrderRepository $orderRepository): void
    {
       $order = $orderRepository->get($event->orderId());

       $distributedBus->convertAndPublishEvent("order.was_placed",[
          "orderId" => $event->orderId(),
          "hadPromotion" => $order->hasPromotion()
       ]);
    }
}

知道你发送什么

Command并且EventsMessages,但是它们中的每一个都存在语义差异。

Command targets specific handler,这意味着只有一个端点处理此消息。另一方面,事件不针对任何东西。它正在发布,感兴趣的各方可能会subscribe。这意味着可能有 20 个订阅者,但也可能没有。

那么如何在分布式环境中处理这个问题。

当我们send命令时,我们应该针对特定的 Service 并在这个 Service 中给定 Handler。这会识别目标服务并确保没有其他服务会处理此命令。

服务名称,就像路由键一样是应用程序级别的一部分。我们需要知道,我们与谁以及为什么要沟通。


<?php

class OrderService
{
    #[EventHandler]
    public function placeOrder(OrderWasPlaced $event, DistributedBus $distributedBus): void
    {
       $distributedBus->convertAndSendCommand(
          "payment_gateway_service",
          "payment.charge_card",
          [
            "userId" => $event->userId(),
            "amount" => $event->price()
          ]
       );
    }
}

class PaymentService
{
    #[Distributed]
    #[CommandHandler("payment.charge_card")]
    public function placeOrder(ChargeCard $command): void
    {
        // charge card
    }
}

它的第一个参数是服务名称,command 将去哪里,第二个参数是处理程序的路由键。

如果有no given handler针对性的服务或Handler没有,消息将失败并可能根据配置distributed登陆。

DLQ在事件的情况下,每个服务也明确声明它想要订阅的事件。

仅订阅和接收给定服务感兴趣的事件,有助于保持消息流的性能。

消息负载可以是任何东西

当前的 PHP 框架已经构建了PHP Message implementation等于Classpayload等于Class.

消息传递原则从来都不是这种情况,消息的有效负载可以是任何 a json/xmlarrayclass甚至是intor simple text string

消息和消息的有效负载都不能是类。

消息具有可以反序列化为 的有效负载,但同时,如果我们希望以这种方式处理它,Class我们应该能够将其反序列化为array甚至保持为。json

如果你只需要意图怎么办?

例如,知道发票已生成,向客户发送短信可能就足够了。你根本不需要知道细节。以这种方式查看消息会创建更松散耦合的接口。

让我们以发布事件为例Order Was Placed

<?php

class OrderService
{
    #[EventHandler]
    public function placeOrder(OrderWasPlaced $event, DistributedBus $distributedBus): void
    {
       $distributedBus->convertAndPublishEvent("order.was_placed",$event);
    }
}

我们可能真的不想创建类,因为我们需要它来检索userId以发送电子邮件。

其他情况可能是我们只想将消息有效负载直接存储在json.

<?php

class NotificationService
{
    #[Distributed]
    #[EventHandler("order.was_placed")]
    public function sendEmail(array $payload): void
    {
        // send email to $payload["userId"];
    }
}


class AuditLogService
{
    #[Distributed]
    #[EventHandler("order.was_placed")]
    public function storeAudit(string $payload): void
    {
        // store json payload
    }
}

在 Ecotone 中,无论消息具有何种内容类型(xml、json、avro、protobuf 等),只要您为其注册了 Converter,您就可以根据需要将其反序列化为类。

一般来说,如果需要,我们甚至可以使用空方法声明。

<?php

class HealthCheck
{
    #[Distributed]
    #[EventHandler("health_check")]
    public function ping(): void
    {
       // do some life check 
    }
}

这将权力交还给开发人员。我们在感到需要时创建课程,而不是因为我们需要。

元数据的东西

如果我们想提供诸如谁是给定动作的执行者,或者它发生的时间,或者可能是一些基础设施信息,比如给定的命令是从哪个域下达的,那该怎么办?

这可能会被添加到payloadduring 步骤中以进行丰富public event。然而,这可能会模糊事件的形象,并且通常可能与它完全无关。

假设我们有多个客户下订单的站点,并且我们希望使用下订单的域来丰富事件的元数据。

<?php

class OrderService
{
    #[EventHandler]
    public function placeOrder(OrderWasPlaced $event, DistributedBus $distributedBus, DomainService $domainService): void
    {
       $distributedBus->convertAndPublishEvent("order.was_placed",
          [
             "orderId" => $event->orderId()
          ],
          [
             "currentDomain" => $domainService->getDomain()
          ]
       );
    }
}

class AuditLogService
{
    #[Distributed]
    #[EventHandler("order.was_placed")]
    public function store(string $event, #[Header("currentDomain")] string $domain): void
    {
        // store order along with domain
    }
}

Ecotone 提供了使用元数据的简单方法,它将负责通过 Message Broker 传递它并允许您在另一端使用它。

概括

随着时间的推移和项目的成熟,我们需要更可靠和长期的集成解决方案。这些解决方案曾在其他被认为更成熟的语言中使用,现在Ecotone将其引入 PHP,因此我们都可以受益并建立在坚实的基础上。

松散耦合是艺术。它通过使它们显式来揭示隐藏的事物。以前看起来很难的事情,变得顺利,感觉很好做。

这就是我们的目标、良好的经验和共同的理解,所以我们可以面带微笑地改变服务。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论