在本文中,我们将深入探讨在 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 Handler
distributed
这让新加入者清楚地知道,从项目的长期来看,哪些处理程序external services
由local ones
.
为了推动这一点,您可以使用Pact添加 Consumer Driven Contract 。
这将让您知道您的消息中的哪些字段被其他服务使用,因此您可以轻松修改未使用的字段。
公共和私人活动
即使我们将明确发布分布式事件并限制其数量,我们仍然可能会被其他服务阻止以进行内部更改。
如果我们要修改payload
或者我们想要删除或替换事件,那么我们最终可能会影响其他方。
这就是为什么值得区分事件public
和private
事件的原因。
通过使用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
并且Events
是Messages
,但是它们中的每一个都存在语义差异。
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
等于Class
或payload
等于Class
.
消息传递原则从来都不是这种情况,消息的有效负载可以是任何 a json/xml
,array
,class
甚至是int
or 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
}
}
这将权力交还给开发人员。我们在感到需要时创建课程,而不是因为我们需要。
元数据的东西
如果我们想提供诸如谁是给定动作的执行者,或者它发生的时间,或者可能是一些基础设施信息,比如给定的命令是从哪个域下达的,那该怎么办?
这可能会被添加到payload
during 步骤中以进行丰富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,因此我们都可以受益并建立在坚实的基础上。
松散耦合是艺术。它通过使它们显式来揭示隐藏的事物。以前看起来很难的事情,变得顺利,感觉很好做。
这就是我们的目标、良好的经验和共同的理解,所以我们可以面带微笑地改变服务。