六、架构模式

架构模式,有时被称为架构样式,为软件架构中反复出现的问题提供解决方案。

虽然类似于软件设计模式,但它们的范围更广,解决了软件工程中的各种问题,而不仅仅是软件本身的开发。

在本章中,我们将介绍以下主题:

  • 模型视图控制器(MVC)
  • 面向服务的体系结构
  • 微服务
  • 异步排队
  • 消息队列模式

模型视图控制器(MVC)

MVC 是 PHP 开发人员遇到的最常见的体系结构模式类型。从根本上说,MVC 是一种用于实现用户界面的体系结构模式。

它主要围绕以下方法工作:

  • 模型:该模型向应用程序提供数据,无论数据来自 MySQL 数据库还是任何其他数据存储。
  • 控制器:控制器本质上就是业务逻辑所在的地方。控制器处理视图提供的任何查询,并使用模型来协助其执行此行为。
  • 查看:提供给最终用户的实际内容。这通常是一个 HTML 模板。

一个交互的业务逻辑并没有严格地与另一个交互分离。应用程序的不同类之间没有正式的分离。

考虑到 MVC 模式主要是 UI 模式,因此它在整个应用程序中不能很好地扩展。这就是说,UI 的呈现越来越多地通过 JavaScript 应用程序完成,JavaScript 应用程序是一个单页 JavaScript HTML 应用程序,它只使用 RESTful API。

如果您使用的是 JavaScript,那么您可以使用诸如 Backbone.js(Model View Presenter)、React.js 或 Angular 之类的框架与后端 API 进行通信,尽管这当然需要一个支持 JavaScript 的 web 浏览器,我们中的一些人可能会认为这是用户理所当然的。

如果您所处的环境无法使用 JavaScript 应用程序,而必须提供呈现的 HTML,那么 MVC 应用程序只需使用 REST API 通常是一个好主意。RESTAPI 执行所有业务逻辑,但标记的呈现是在 MVC 应用程序中完成的。尽管这增加了复杂性,但它提供了更大的职责分离,因此,您不需要将 HTML 与核心业务逻辑合并。也就是说,即使在这个 RESTAPI 中,您也需要某种形式的关注点分离,您需要能够将标记的呈现与实际业务逻辑分离。

选择适合应用程序的体系结构模式的一个关键因素是复杂性是否适合应用程序的大小。因此,选择 MVC 框架也应该基于应用程序本身的复杂性及其以后的预期复杂性。

考虑到基础设施作为代码的增长,可以以完全协调的方式部署多个 web 服务的基础设施。事实上,使用容器化技术(如 Docker)可以部署多个体系结构(如具有单独 API 服务的 MVC 应用程序),而开销很小(无需为每个服务启动新服务器)。

在开发伟大的体系结构时,关注点分离是一个至关重要的特性,包括将 UI 与业务逻辑分离。

当按照 MVC 模式思考时,记住以下交互非常重要:

  • 模型存储数据,这些数据根据模型放置的查询进行检索,并由视图显示
  • 视图根据对模型的更改生成输出
  • 控制器发送更新模型状态的命令;它还可以更新与其关联的视图,以改变给定模型的显示方式

或者,通常使用下图表示:

Model-View-Controller (MVC)

不要为了使用 MVC 框架而使用 MVC 框架,要理解它们存在的原因以及它们在哪里可以很好地适应用例。请记住,当您使用一个具有大量功能的臃肿框架时,您将负责维护整个过程。

在开发具有大量业务逻辑的软件时,根据需要引入组件(即通过 Composer)是一种更加实用的方法。

面向服务的体系结构

面向服务的体系结构主要由与数据存储库通信的服务中的业务逻辑组成。

这些服务可以以不同的形式派生以构建应用程序。这些应用程序以不同的格式采用这些服务来构建各种应用程序。将服务视为乐高块,可以将它们集成在一个给定格式中构建应用程序。

这种描述相当粗糙;让我进一步澄清:

  • 服务的边界是明确的(它们可以在不同的域上分离 web 服务,等等)
  • 服务可以使用公共通信协议进行交互通信(例如,所有服务都使用 RESTful API)
  • 服务是自治的(它们是解耦的,与另一个服务没有任何关系)
  • 消息处理机制和模式可以被其他微服务理解(因此通常是相同的),但编程环境可能不同

面向服务的体系结构本质上是分布式的,因此它们比其他体系结构具有更高的前端复杂性。

微服务

微服务体系结构可以被视为面向服务体系结构的子集。

从根本上说,微服务通过组成一个独立的小流程形成复杂的应用程序,该流程通过一个语言无关的 API 进行交互,使得每个服务都可以相互访问。微服务可以作为服务单独部署。

在微服务中,业务逻辑被分离为自包含的松散耦合服务。微服务的一个关键原则是每个数据库都应该有自己的数据库,这对于确保微服务不会彼此紧密耦合至关重要。

通过降低单个服务的复杂性,我们可以减少该服务失败的点的数量。从理论上讲,通过使单个服务符合单一责任原则,可以更容易地在整个应用程序中进行调试并减少失败的机会。

在计算机科学中,CAP 定理指出,在给定的分布式计算机系统中,不可能同时保证一致性、可用性和分区容差。

设想两个分布式数据库都包含用户的电子邮件地址。如果我们想更新此电子邮件地址,我们无法通过在不将两个数据集恢复到一起的情况下,同时在两个数据库中即时更新电子邮件地址的方式来实现。在分布式系统中,我们必须延迟对数据的访问以验证数据的一致性,或者提供数据的未更新副本。

这使得传统的数据库事务处理变得困难。因此,在微服务体系结构中解决数据处理问题的最佳方法是使用最终一致的事件驱动体系结构。

每当发生更改时,每个服务都会发布一个事件,其他服务可能会订阅该事件。当接收到事件时,相应地更新数据。因此,应用程序能够跨多个服务维护数据一致性,而无需使用分布式事务。

为了了解如何为微服务之间的通信实现这样的进程间通信架构,请参阅本章中的消息队列模式(RabbitMQ 入门)部分。

在这种情况下,缓解这种限制的一种简单方法是使用时间验证系统来验证数据的一致性。因此,我们放弃了一致性和分区容差的可用性。

如果可以预见这是给定微服务体系结构中的一个问题,那么最好将需要满足 CAP 定理的服务组合到一个服务中。

让我们考虑一个由以下微服务组成的比萨饼递送 Web 应用程序:

  • 使用者
  • 交易
  • 配方
  • 运货马车
  • 演员表
  • 付款
  • 餐馆
  • 传送
  • 披萨
  • 评论
  • 前端微服务

在本例中,我们可以进行以下用户旅程:

  1. 使用用户微服务对用户进行身份验证。
  2. 用户可以使用 Deals microservice 选择优惠。
  3. 用户使用 Recipe microservice 选择他们想要订购的比萨饼。
  4. 使用购物车微服务将选定的比萨饼添加到购物车中。
  5. 通过计费微服务优化计费凭证。
  6. 用户使用 Payments microservice 支付。
  7. 订单通过餐厅微服务发送到餐厅。
  8. 当餐厅烹调好食物后,送货微型服务会发送一名司机来收集食物并送货。
  9. 一旦 Delivery microservice 指示食物已送达,用户将被邀请使用 review microservice 完成审查(该服务会通知用户使用 user microservice)。
  10. 该网站的 web 前端使用前端微服务包装在一起。

前端微服务可以只是一种微服务,它使用其他微服务并将内容呈现给 web 前端。该前端可以通过 REST 与其他微服务通信,可能是在浏览器中的 JavaScript 客户端中实现的,或者只是充当其他微服务 API 使用者的 PHP 应用程序中实现的。

无论哪种方式,在 API 的前端使用者和后端使用者之间放置网关通常是一个好主意。这允许我们在确定与微服务的通信之前放置一些中间件;例如,我们可以使用网关查询用户微服务,在允许访问购物车微服务之前检查用户是否已获得授权。

如果您使用 JavaScript 直接与微服务通信,当 web 前端尝试在不同主机名/端口上与微服务通信时,您可能会发现跨源问题;微服务网关可以通过将网关与 web 前端本身放在同一来源上来帮助防止这种情况。

作为对网关便利性的交换,您可能会感觉到它的缺点,因为您需要担心另一个系统和额外的响应时间(尽管您可以在网关级别添加缓存,以提高性能)。

由于增加了网关,我们的体系结构现在可以如下所示:

Microservices

PHP 中越来越多的出现了微框架,如 Lumen、Silex 和 Slim;这些是面向 API 的框架,使构建微服务以支持我们的应用程序变得容易。也就是说,您通常最好采用一种更轻量级的方法,在需要时通过 Composer 只引入所需的组件。

请记住,添加其他技术或框架会增加总体情况的复杂性。不仅要考虑实施新解决方案的技术原因,还要考虑这将如何使客户和体系结构受益。微服务不是增加不必要复杂性的借口:保持简单,愚蠢

异步排队

消息队列提供异步通信协议。在异步通信协议中,发送方和接收方不需要同时与消息队列交互。

另一方面,典型的 HTTP 是一种同步通信协议,这意味着在操作完成之前,客户端将被阻塞。

考虑这一点;你给某人打电话,然后等电话铃响,和你说话的人听你当时说的话。在交流结束时,你说再见,而另一端的人也承认你说了再见。这可以被认为是同步的,因为在收到与你交流的人的回复以结束交流之前,你不会做任何事情。

然而,如果你要给某人发短信,在你发完短信后,你可以去做任何你想做的事情;当他们想要回复您时,您可以收到一条回复您发送的消息。当有人起草回信时,你可以去做任何你想做的事情。虽然您不直接与发件人通信,但您仍然与手机保持同步通信,当您收到新消息时,手机会通知您(或者只需每隔几分钟检查一次手机);但是与另一方本身的通信是异步的。任何一方都不需要了解对方的任何情况,他们只是在寻找自己的短信以便相互交流。

消息队列模式(RabbitMQ 入门)

RabbitMQ 是一个消息代理;它接受并转发消息。在这里,让我们对其进行配置,以便可以将消息从一个 PHP 脚本发送到另一个 PHP 脚本。

想象一下,我们给快递员一个包裹,让他们给客户;RabbitMQ 是信使,而脚本是分别接收和发送包的个人。

作为第一步,让我们安装 RabbitMQ;我将在 Ubuntu 14.04 系统上演示这一点。

首先,我们需要将 RabbitMQ APT 存储库添加到我们的/etc/apt/sources.list.d文件夹中。幸运的是,可以使用如下命令执行此操作:

echo 'deb http://www.rabbitmq.com/debian/ testing main' | sudo tee /etc/apt/sources.list.d/rabbitmq.list

请注意,存储库可能会发生更改;如果有,您可以在找到最新的详细信息 https://www.rabbitmq.com/install-debian.html

我们还可以选择将 RabbitMQ 公钥添加到受信任密钥列表中,以避免在通过apt命令安装或升级软件包时出现任何指示软件包未签名的警告:

wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add -

到目前为止,一切顺利:

Message Queue pattern (Getting started with RabbitMQ)

接下来,让我们运行一个apt-get update命令,从包含的新存储库中获取包。完成后,我们可以使用apt-get install rabbitmq-server命令开始安装所需的软件包:

Message Queue pattern (Getting started with RabbitMQ)

当被问及以下问题时,请务必接受各种提示:

Message Queue pattern (Getting started with RabbitMQ)

安装完成后,您可以运行rabbitmqctl status检查应用程序的状态,检查其运行是否正常:

Message Queue pattern (Getting started with RabbitMQ)

让我们的生活轻松一点。我们可以使用 web GUI 来管理 RabbitMQ;只需运行以下命令:

rabbitmq-plugins enable rabbitmq_management

Message Queue pattern (Getting started with RabbitMQ)

我们现在可以在<your server IP here>:15672看到一个管理界面:

Message Queue pattern (Getting started with RabbitMQ)

但在登录之前,我们必须创建一些登录凭据。为了做到这一点,我们必须回到命令行。

首先,我们需要设置一个新帐户,用户名为junade,密码为insecurepassword

rabbitmqctl add_user junade insecurepassword

然后我们可以添加一些管理员权限:

rabbitmqctl set_user_tags junade administrator
rabbitmqctl set_permissions -p / junade ".*" ".*" ".*"

返回登录页面,在输入以下凭据后,我们现在可以看到酷管理员界面:

Message Queue pattern (Getting started with RabbitMQ)

这是 RabbitMQ 服务的 web 界面,可通过我们的 web 浏览器访问

现在我们可以测试我们安装的东西了。让我们先为这个新项目写一个composer.json文件:

{ 
  "require": { 
    "php-amqplib/php-amqplib": "2.5.*" 
  } 
} 

RabbitMQ 使用高级消息队列协议AMQP),这就是为什么我们要安装一个 PHP 库,它将基本上帮助我们通过该协议与它通信。

接下来,我们可以编写一些代码,使用刚刚安装的 RabbitMQ 消息代理发送消息:

这假设端口为5672且安装在localhost上,这可能会根据您的情况发生变化。

让我们编写一个小 PHP 脚本来利用这一点:

<?php 

require_once(__DIR__ . '/vendor/autoload.php'); 
use PhpAmqpLib\Connection\AMQPStreamConnection; 
use PhpAmqpLib\Message\AMQPMessage; 

$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword'); 
$channel    = $connection->channel(); 

$channel->queue_declare( 
  'sayHello',     // queue name 
  false,          // passive 
  true,           // durable 
  false,          // exclusive 
  false           // autodelete 
); 

$msg = new AMQPMessage("Hello world!"); 

$channel->basic_publish( 
  $msg,           // message 
  '',             // exchange 
  'sayHello'      // routing key 
); 

$channel->close(); 
$connection->close(); 

echo "Sent hello world message." . PHP_EOL; 

让我们把它分解一下。在前几行中,我们只包括来自 Composerautoloadstate的库,我们将使用哪些名称空间。当我们实例化AMQPStreamConnection对象时,我们实际上连接到 MessageBroker;然后,我们可以创建一个新的通道对象,然后使用该对象声明一个新队列。我们通过调用queue_declare消息来声明队列。持久选项允许消息在 RabbitMQ 中重新启动后仍然有效。最后,我们继续发出我们的信息。

现在让我们运行以下脚本:

php send.php

其输出如下所示:

Message Queue pattern (Getting started with RabbitMQ)

如果现在转到 RabbitMQ 的 web 界面,请单击队列选项卡并切换获取消息对话框;您应该能够获取我们刚刚发送给代理的消息:

Message Queue pattern (Getting started with RabbitMQ)

在界面中使用此网页,我们可以从队列中提取消息,以便查看其内容

当然,这只是故事的一半。我们现在需要使用另一个应用程序实际检索此消息。

让我们写一个receive.php脚本:

<?php 

require_once(__DIR__ . '/vendor/autoload.php'); 
use PhpAmqpLib\Connection\AMQPStreamConnection; 
use PhpAmqpLib\Message\AMQPMessage; 

$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword'); 
$channel    = $connection->channel(); 

$channel->queue_declare( 
  'sayHello',     // queue name 
  false,          // passive 
  false,          // durable 
  false,          // exclusive 
  false           // autodelete 
); 

$callback = function ($msg) { 
  echo "Received: " . $msg->body . PHP_EOL; 
}; 

$channel->basic_consume( 
  'sayHello',                     // queue 
  '',                             // consumer tag 
  false,                          // no local 
  true,                           // no ack 
  false,                          // exclusive 
  false,                          // no wait 
  $callback                       // callback 
); 

while (count($channel->callbacks)) { 
  $channel->wait(); 
} 

请注意,前几行与发送脚本相同;我们甚至重新声明队列,以防在运行send.php脚本之前运行此接收脚本。

让我们运行我们的receive.php脚本:

Message Queue pattern (Getting started with RabbitMQ)

在另一个 bash 终端中,让我们运行几次send.php脚本:

Message Queue pattern (Getting started with RabbitMQ)

相应地,在receive.php终端选项卡中,我们现在可以看到我们已经收到我们发送的消息:

Message Queue pattern (Getting started with RabbitMQ)

RabbitMQ 文档使用下图描述消息的基本接收和转发:

Message Queue pattern (Getting started with RabbitMQ)

发布订户模式

发布订阅者模式(简称 Pub/Sub)是一种设计模式,消息不会直接从发布者发送到订阅者;相反,出版商在不知情的情况下发布信息。

在 RabbitMQ 中,生产者从不直接向队列发送任何消息。通常,生产者甚至不知道消息是否会在队列中结束。相反,制作人必须向交换机发送消息。它接收来自生产者的消息,然后将它们推出队列。

使用者是将接收消息的应用程序。

必须准确地告诉交换机如何处理给定的消息,以及应该将消息附加到哪个队列。这些规则由交换类型定义。

RabbitMQ 文档描述了发布订阅者关系(连接发布者、exchange、队列和使用者),如下所示:

Publish-Subscriber pattern

直接交换类型基于路由密钥传递消息。它可以用于一对一和一对多的路由形式,但最适合一对一的关系。

扇出交换类型将消息路由到绑定到它的所有队列,并且完全忽略路由密钥。实际上,您无法根据路由密钥区分将向哪些工作人员分发消息。

主题交换类型根据消息路由队列和用于将队列绑定到交换的模式,将消息路由到一个或多个队列。当多个消费者/应用程序想要选择他们想要接收的消息类型时,这种交换可能会很好地工作,通常是在多对多关系中。

交换类型通常用于在一组属性上进行路由,这些属性在消息头中的表达比路由队列更好。路由的属性基于 headers 属性,而不是使用路由键。

为了测试发布/子队列,我们将使用以下脚本。它们与前面示例中的类似,只是我对它们进行了修改,以便使用交换。这是我们的send.php文件:

<?php 

require_once(__DIR__ . '/vendor/autoload.php'); 
use PhpAmqpLib\Connection\AMQPStreamConnection; 
use PhpAmqpLib\Message\AMQPMessage; 

$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword'); 
$channel    = $connection->channel(); 

$channel->exchange_declare( 
  'helloHello',   // exchange 
  'fanout',       // exchange type 
  false,          // passive 
  false,          // durable 
  false           // auto-delete 
); 

$msg = new AMQPMessage("Hello world!"); 

$channel->basic_publish( 
  $msg,           // message 
  'helloHello'    // exchange 
); 

$channel->close(); 
$connection->close(); 

echo "Sent hello world message." . PHP_EOL; 

这是我们的receive.php档案。与之前一样,我修改了此脚本,以便它也使用 Exchange:

<?php 

require_once(__DIR__ . '/vendor/autoload.php'); 
use PhpAmqpLib\Connection\AMQPStreamConnection; 
use PhpAmqpLib\Message\AMQPMessage; 

$connection = new AMQPStreamConnection('localhost', 5672, 'junade', 'insecurepassword'); 
$channel    = $connection->channel(); 

$channel->exchange_declare( 
  'helloHello',   // exchange 
  'fanout',       // exchange type 
  false,          // passive 
  false,          // durable 
  false           // auto-delete 
); 

$callback = function ($msg) { 
  echo "Received: " . $msg->body . PHP_EOL; 
}; 

list($queueName, ,) = $channel->queue_declare("", false, false, true, false); 

$channel->queue_bind($queueName, 'helloHello'); 

$channel->basic_consume($queueName, '', false, true, false, false, $callback); 

while (count($channel->callbacks)) { 
  $channel->wait(); 
} 

$channel->close(); 
$connection->close(); 

现在,让我们测试这些脚本。我们首先需要运行receive.php脚本,然后我们可以使用send.php脚本发送消息。

首先,让我们触发receive.php脚本,让它开始运行:

Publish-Subscriber pattern

完成后,我们可以通过运行send.php脚本继续发送消息:

Publish-Subscriber pattern

这将使用以下信息填充我们正在运行的终端receive.php

Publish-Subscriber pattern

总结

在本章中,我们学习了架构模式。从 MVC 开始,我们了解了使用 UI 框架的好处和挑战,并讨论了如何以更严格的方式将 UI 与业务逻辑解耦。

然后,我们转向 SOA,了解了它与微服务的比较,以及考虑到分布式系统所带来的挑战,这些架构在哪里有意义。

最后,我们深入地介绍了排队系统,它们在哪里合适,以及如何在 RabbitMQ 中实现它们。

在下一章和最后一章中,我们将介绍架构模式的最佳实践使用条件。