微服务的注册和发现

微服务的注册和发现

一、微服务设计,服务发现

微服务设计——服务发现

导语

在分布式微服务架构中,一个应用可能由一组职责单一化的服务组成。这时候就需要一个注册服务的机制,注册某个服务或者某个节点是可用的,还需要一个发现服务的机制来找到哪些服务或者哪些节点还在提供服务。
在实际应用中,通常还都需要一个配置文件告诉我们一些配置信息,比如数据连接的地址,redis 的地址等等。但很多时候,我们想要动态地在不修改代码的情况下得到这些信息,并且能很好地管理它们。
有了这些需求,于是发展出了一系列的开源框架,比如 ZooKeeper, Etcd, Consul 等等。

这些框架一般会提供这样的服务:

  • 服务注册
    主机名,端口号,版本号,或者一些环境信息。

  • 服务发现
    可以让客户端拿到服务端地址。

  • 一个分布式的通用 k/v 存储系统
    基于 Paxos 算法或者 Raft 算法

  • 领导者选举 (Leader Election)

其它一些例子:

  • 分布式锁 (Distributed locking)
  • 原子广播 (Atomic broadcast)
  • 序列号 (Sequence numbers)

我们都知道 CAP 是 Eric Brewer 提出的分布式系统三要素:

  • 强一致性 (Consistency)
  • 可用性 (Availability)
  • 分区容忍性 (Partition Tolerance)

几乎所有的服务发现和注册方案都是 CP 系统,也就是说满足了在同一个数据有多个副本的情况下,对于数据的更新操作达到最终的效果和操作一份数据是一样的,但是在出现网络分区(分区间的节点无法进行网络通信)这样的故障的时候,在节点之间恢复通信并且同步数据之前,一些节点将会无法提供服务(一些系统在节点丢失的情况下支持 stale reads )。

1 什么是服务发现?

服务发现组件记录了(大规模)分布式系统中所有服务的信息,人们或者其它服务可以据此找到这些服务。DNS 就是一个简单的例子。当然,复杂系统的服务发现组件要提供更多的功能,例如,服务元数据存储、健康监控、多种查询和实时更新等。服务发现是支撑大规模 SOA 的核心服务

2 服务发的关键特性

  • 高可用的
  • 服务目录
  • 服务查找
  • 服务注册

3 为什么要使用服务发现

假设我们写的代码会调用 WebService、Rest Api、 Thrift API 的服务。在调用过程中,为了完成一次请求,代码需要知道服务实例的网络位置(IP 地址和端口)。

整个过程,对于基于云端的、现代化的微服务应用而言,这却是一大难题。为了更好的让大家了解服务发现的发展过程,现在举个例子。

##### 3.1 单体应用

假设你是项目经理或者公司的架构师,正准备组织团队开发一款产品,类似滴滴与Uber的出租车调度软件。
其中系统的核心业务有:客户端、司机端、定位、通知、支付
传统的架构图为:六边形架构(即模块化的单体是应用),也称单体式应用,如下图

六边形架构.png

单体应用的不足

这种简单方法却有很大的局限性。
一个简单的应用会随着时间推移逐渐变大。在每次的迭代中,开发团队都会面对新“故事”(需求),然后开发许多新代码。
几年后,这个小而简单的应用会变成了一个巨大的怪物

如果有经验的管理者都知道,一旦你的应用变成一个又大又复杂的怪物,那开发团队肯定很痛苦。
敏捷开发和部署举步维艰,其中最主要问题就是这个应用太复杂,以至于任何单个开发者都不可能搞懂它。

  1. 降低开发速度
  2. 不利于持续性开发
  3. 模块相互冲突
  4. 可靠性低
  5. 重构困难
##### 3.2 微服务

随着时间的发展和项目的发展,业务团队越来越庞大,业务越来越复杂,单体应用架构已经无法满足项目需求,所以微服务就腾空出世了。
许多公司,比如Amazon、eBay,通过采用微处理结构模式解决了单体应用出现的问题。
其思路不是开发一个巨大的单体式的应用,而是将应用分解为小的、互相连接的微服务

微服务架构.png

微服务架构的好处

  1. 单个服务很容易开发、理解和维护。
  2. 这种架构使得每个服务都可以有专门开发团队来开发。
  3. 微服务架构模式是每个微服务独立的部署。
  4. 微服务架构模式使得每个服务独立扩展。

微服务架构的不足

微服务应用是分布式系统,由此会带来固有的复杂性。
服务地址目录,服务健康度,部署困难,服务依赖问题,数据库分区问题。

如何解决微服务出现的这些问题呢?服务发现框架在这时就闪亮登场了。

4 常见的服务发现框架有哪些

常见服务发现框架 Consul、 ZooKeeper以及Etcd
ZooKeeper是这种类型的项目中历史最悠久的之一,它起源于Hadoop。它非常成熟、可靠,被许多大公司(YouTube、eBay、雅虎等)使用。
etcd是一个采用HTTP协议的健/值对存储系统,它是一个分布式和功能层次配置系统,可用于构建服务发现系统。其很容易部署、安装和使用,提供了可靠的数据持久化特性。它是安全的并且文档也十分齐全。

名称 Zookeeper etcd Consul
产生时间
原生语言 JAVA Go Go
算法 Paxos Raft Raft
多数据中心 不支持 不支持 支持
健康检查 支持 不支持 支持
web管理界面 较为复杂 不支持 支持
http协议 较为复杂 支持 支持
DNS协议 较为复杂 不支持 支持

5 Consul服务发现框架介绍

Consul是强一致性的数据存储,使用gossip形成动态集群。它提供分级键/值存储方式,不仅可以存储数据,而且可以用于注册器件事各种任务,从发送数据改变通知到运行健康检查和自定义命令,具体如何取决于它们的输出。

下面两张图是Consul的原理图

Consul的原理图01.png
Consul的原理图02.png

Consul 和 Etcd 一样也有两种节点,一种叫 client (和 Etcd 的 proxy 一样) 只负责转发请求,另一种是 server,是真正存储和处理事务的节点。

Consul 使用基于 Serf 实现的 gossip 协议来管理从属关系,失败检测,事件广播等等。gossip 协议是一个神奇的一致性协议,之所以取名叫 gossip 是因为这个协议是模拟人类中传播谣言的行为而来。要传播谣言就要有种子节点,种子节点每秒都会随机向其它节点发送自己所拥有的节点列表,以及需要传播的消息。任何新加入的节点,就在这种传播方式下很快地被全网所知道。这个协议的神奇就在于它从设计开始就没想要信息一定要传递给所有的节点,但是随着时间的增长,在最终的某一时刻,全网会得到相同的信息。当然这个时刻可能仅仅存在于理论,永远不可达。

Consul 使用了两个不同的 gossip pool,分别叫做 LAN 和 WAN,这是因为 Consul 原生支持多数据中心。在一个数据中心内部,LAN gossip pool 包含了这个数据中心所有的节点,包括 proxy 和 servers。WAN pool 是全局唯一的,所有数据中心的 servers 都在这个 pool 中。这两个 pool 的区别就是 LAN 处理的是数据中心内部的失败检测,事件广播等等,而 WAN 关心的是跨数据中心的。除了 gossip 协议之外,Consul 还使用了** Raft 协议**来进行 leader election,选出 leader 之后复制日志的过程和 Etcd 基本一致。

回到应用层面上来说,Consul 更像是一个 full stack 的解决方案,它不仅提供了一致性 k/v 存储,还封装了服务发现,健康检查,内置了 DNS server 等等。这看上去非常美好,简直可以开箱即用。于是,我们初步选定了 Consul 作为我们的服务发现和动态配置的框架。但现实往往是冰冷的,在深入研究它的 API 之后发现了比较多的坑,可能设计者有他自己的考虑。

在使用获取所有 services 的 API 的时候返回的只是所有服务的名字和 tag,如果想要每个服务的具体信息,你还需要挨个去请求。这在客户端就会十分不方便,因为在笔者看来,获取所有服务的列表以及具体信息应该是一个很常见的需求,并且请求的次数越少越好。

如果 watch 服务是否有变化,当值发生改变的时候,返回的结果居然是相当于重新读取所有 services,没有能够体现出服务信息的变化,这会导致客户端很难进行更新操作。

健康检查是 Consul 的内置功能,在注册一个服务的时候可以加上自定义的健康检查,这听上去很不错。但是如果你忘记给你某个服务加上健康检查,那它在各种 API 的返回结果中变得难以预料。

6 ZooKeeper

ZooKeeper 作为这类框架中历史最悠久的之一,是由 Java 编写。

7 Etcd

Etcd 是一个使用 Go 语言写的分布式 k/v 存储系统。考虑到它是 coreos 公司的项目,以及在 GitHub 上的 star 数量 (9000+),Google 著名的容器管理工具 Kuberbetes 就是基于 Etcd 的
在介绍 Etcd 之前,我们需要了解一些基本的概念。我们知道在单台服务器单个进程中维护一个状态是很容易的,读写的时候不会产生冲突。即便在多进程或者多线程环境中,使用锁机制或者协程(coroutine)也可以让读写有序地进行。但是在分布式系统中,情况就会复杂很多,服务器可能崩溃,节点间的机器可能网络不通等等。所以一致性协议就是用来在一个集群里的多台机器中维护一个一致的状态。
很多的分布式系统都会采用 Paxos 协议,但是 Paxos 协议难以理解,并且在实际实现中差别比较大。所以 Etcd 选择了** Raft 作为它的一致性协议**。Raft 是 Diego Ongaro 和 John Ousterhout 在 ‘In Search of an Understandable Consensus Algorithm’ 中提出的。它在牺牲很少可用性,达到相似功能的情况下,对 Paxos 做了很大的优化,并且比 Paxos 简单易懂很多。

它主要集中在解决两个问题:

  • 领导者选举(Leader Election)
    Raft 先通过领导选举选出一个 Leader,后续的一致性维护都由 Leader 来完成,这就简化了一致性的问题。Raft 会保证一个时间下只会有一个 Leader,并且在超过一半节点投票的情况下才会被选为 Leader。当 Leader 挂掉的时候,新的 Leader 将会被选出来。

  • 日志复制 (Log Replication)
    为了维护状态,系统会记录下来所有的操作命令日志。Leader 在收到客户端操作命令后,会追加到日志的尾部。然后 Leader 会向集群里所有其它节点发送 AppendEntries RPC 请求,每个节点都通过两阶段提交来复制命令,这保证了大部分的节点都能完成。

在实际的应用中,一般 Etcd 集群以 5 个或者 7 个为宜,可以忍受 2 个或者 3 个节点挂掉,为什么不是越多越好呢?是出于性能的考虑,因为节点多了以后,日志的复制会导致 CPU 和网络都出现瓶颈。

Etcd 的 API 比较简单,可以对一个目录或者一个 key 进行 GET,PUT,DELETE 操作,是基于 HTTP 的。Etcd 提供 watch 某个目录或者某个 key 的功能,客户端和 Etcd 集群之间保持着长连接 (long polling)。基于这个长连接,一旦数据发生改变,客户端马上就会收到通知,并且返回的结果是改变后的值和改变前的值,这一点在实际应用中会很有用(这也是后面的 Consul 的槽点之一)。

Etcd 的 watch 和在一般情况下不会漏掉任何的变更。因为 Etcd 不仅存储了当前的键值对,还存储了最近的变更记录,所以如果一个落后于当前状态的 watch 还是可以通过遍历历史变更记录来获取到所有的更新。Etcd 还支持 CompareAndSwap 这个原子操作,首先对一个 key 进行值比较,只有结果一致才会进行下一步的赋值操作。利用这个特性就像利用 x86 的 CAS 实现锁一样可以实现分布式锁。

在 Etcd 中有个 proxy 的概念,它其实是个转发服务器,启动的时候需要指定集群的地址,然后就可以转发客户端的请求到集群,它本身不存储数据。一般来说,在每个服务节点都会启动一个 proxy,所以这个 proxy 也是一个本地 proxy,这样服务节点就不需要知道 Etcd 集群的具体地址,只需要请求本地 proxy。之前提到过一个 k/v 系统还应该支持 leader election,Etcd 可以通过 TTL (time to live) key 来实现。

二、微服务订阅

使用事件总线的第一步是订阅他们想要接收的事件微服务。 应该在接收方微服务来完成。

下面的简单代码显示每个接收方 microservice 需要实现启动服务时 (即,在启动类) 以便它所订阅的事件它需要。 例如,basket.api microservice 需要订阅 ProductPriceChangedIntegrationEvent 消息。 这对产品价格进行 microservice 了解的任何更改,并允许它则在用户的购物篮中是该产品的情况下发出警告的更改的相关用户。

var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>();
eventBus.Subscribe<ProductPriceChangedIntegrationEvent>(
    ProductPriceChangedIntegrationEventHandler);

此代码运行后,订阅服务器微服务将侦听通过 RabbitMQ 通道中。 当类型 ProductPriceChangedIntegrationEvent 的任何消息到达时,则代码将调用的事件处理程序传递给它并处理该事件。

通过事件总线发布事件

最后,消息发送方 (原点 microservice) 发布具有类似于下面的示例代码的集成事件。 (这是不会考虑在内的原子性的简化的示例。)每当必须跨多个微服务,在提交的数据或从原点 microservice 事务后通常右传播事件时,将实现类似的代码。

首先,将在控制器构造函数,如以下代码所示插入事件总线实现对象 (基于 RabbitMQ 或基于服务总线):

[Route("api/v1/[controller]")]
public class CatalogController : ControllerBase
{
    private readonly CatalogContext _context;
    private readonly IOptionsSnapshot<Settings> _settings;
    private readonly IEventBus _eventBus;

    public CatalogController(CatalogContext context,
        IOptionsSnapshot<Settings> settings,
        IEventBus eventBus)
    {
        _context = context;
        _settings = settings;
        _eventBus = eventBus;
    }
    // ...
}

然后你将其用于从控制器的方法,如 UpdateProduct 方法:

[Route("update")]
[HttpPost]
public async Task<IActionResult> UpdateProduct([FromBody]CatalogItem product)
{
    var item = await _context.CatalogItems.SingleOrDefaultAsync(
        i => i.Id == product.Id);
    // ...
    if (item.Price != product.Price)
    {
        var oldPrice = item.Price;
        item.Price = product.Price;
        _context.CatalogItems.Update(item);
        var @event = new ProductPriceChangedIntegrationEvent(item.Id,
            item.Price,
            oldPrice);
        // Commit changes in original transaction
        await _context.SaveChangesAsync();
        // Publish integration event to the event bus
        // (RabbitMQ or a service bus underneath)
        _eventBus.Publish(@event);
        // ...
    }
    // ...
}

在这种情况下,由于源 microservice 是简单 CRUD 微服务,该代码位于右转变为 Web API 控制器。 在更高级的微服务,它无法实现在 CommandHandler 类中,右提交的原始数据后。

发布到事件总线时设计原子性和复原能力

发布通过分布式消息传递集成事件时系统喜欢事件 bus,你可以以原子方式更新原始数据库和发布事件的问题。 例如,在前面所示简化示例中,代码提交数据到数据库时产品价格已更改,然后发布 ProductPriceChangedIntegrationEvent 消息。 最初,可能看起来基本以原子方式执行这两个操作。 但是,如果你使用分布式的事务涉及数据库和消息代理,如在较旧的系统,如Microsoft 消息队列 (MSMQ),不推荐针对所描述的原因,这CAP 定理

基本上,你可以使用微服务来生成可缩放且高度可用的系统。 CAP 定理简化某种程度上,指出无法生成数据库 (或拥有其模型 microservice) 持续可用、 坚实的一致性,允许的任何分区。 你必须选择两个这三个属性。

中基于微服务的体系结构,应选择可用性和容错能力,并应降低强一致性。 因此,在大多数现代基于微服务构成的应用程序,你通常不希望使用消息传送中的分布式的事务实现时那样分布式事务基于 Windows 的分布式事务协调器 (DTC) 与使用MSMQ

让我们回到初始问题和其示例。 如果在服务崩溃后更新数据库 (在这种情况下,使用代码的行后右键_上下文。SaveChangesAsync()),但在集成事件发布之前,总体系统可能会变得不一致。 这可能是业务非常重要,具体取决于你处理的特定业务操作。

如前文所体系结构节中,你可以通过几种方法可以处理此问题:

对于此方案中,使用完整的事件来源 (ES) 模式是之一的最佳方法,如果不最佳。 但是,在许多应用程序方案中,你可能不能实现完整的 ES 系统。 ES 意味着将仅域事件存储在事务数据库中,而不是存储当前状态数据。 存储仅域事件可能会很多好处,例如具有你可用的系统的历史记录,并且能够在过去任意时间点确定你的系统的状态。 但是,实现完整的 ES 系统需要构建你的系统的大部分并且引入了许多其他复杂性和要求。 例如,你想要使用数据库专门所做的事件来源,如事件存储,或如 Azure Cosmos DB、 MongoDB、 Cassandra、 CouchDB 或 RavenDB 面向文档的数据库。 ES 是非常好的方法,此问题,但不是最简单的解决方案,除非你已熟悉事件来源。

使用最初挖掘的事务日志的选项看起来非常透明。 但是,若要使用此方法,microservice 程序耦合到你 RDBMS 的事务日志,如 SQL Server 事务日志。 这可能是不可取。 另一个缺点是在与高级集成事件相同的级别可能不是在事务日志中记录的低级别更新。 如果是这样,反向工程处理的过程这些事务日志操作可能很困难。

平衡的方法是事务的数据库表和简化的 ES 模式的组合。 你可以使用如”已准备好发布事件,”在原始事件时将其提交到集成事件表中设置的状态。 然后尝试将事件发布到事件总线。 如果发布事件操作成功,你在 origin 服务中启动另一个事务,并将状态从”准备好发布事件”移到”事件已发布”。

如果发布事件操作在事件总线失败,数据仍将不会原点 microservice 内不一致-它仍标记为”已准备好发布事件,”,针对服务的其余部分,它最终将一致。 你始终可以检查事务或集成事件的状态的后台作业。 如果作业中的”准备好发布事件”状态中查找事件,它可以尝试重新发布到事件总线该事件。

请注意,使用此方法,保存在你想要与其他微服务或外部系统通信的事件和仅每个源 microservice,集成事件。 与此相反,在完整 ES 系统中,您存储所有域事件。

因此,此平衡的方法是一个简化的 ES 系统。 你需要使用其当前状态的集成事件的列表 (”准备发布”与”发布”)。 但你只需以实现集成事件这些状态。 并在此方法中,你不必将所有域数据都存储为事件在事务的数据库中,就像在完整的 ES 系统。

如果你已使用关系数据库,可以使用事务的表来存储集成事件。 若要实现应用程序中的原子性,你可以使用两步骤过程基于本地事务。 基本上,你必须域实体位于同一数据库中有一个 IntegrationEvent 表。 该表的工作与实现原子性,使你包含的保险保存到同一个事务正在提交你的域数据集成事件。

这个过程步骤,可以如下: 应用程序开始一个本地数据库事务。 然后,将更新你的域实体的状态,并将事件插入到集成事件表。 最后,它会将提交事务。 获取所需的原子性。

在实现时发布事件的步骤,你可选择以下选项:

  • 提交事务后立即发布集成事件并使用另一个本地事务将标记与正在发布的表中的事件。 然后,就像项目中使用表来跟踪远程微服务,在问题发生的集成事件并执行基于存储的集成事件的补偿性操作。

  • 表可以用作一种类型的队列。 单独的应用程序线程或进程查询集成事件表,将事件发布到事件总线,然后使用本地事务将事件标记为已发布。

图 8-22 显示这些方法的第一个体系结构。

图 8-22。 在将事件发布到事件总线原子性

阐释图 8-22 的方法缺少都负责检查和确认的已发布的集成事件成功附加辅助进程微服务。 如果出现故障,该检查器中其他辅助微服务可以从表读取事件,并重新发布它们。

有关第二种方法: 使用事件日志表作为队列和始终使用辅助微服务来发布消息。 在这种情况下,该过程是类似的显示图 8-23。 下面的示例演示其他的微服务,并且表比较的单个来源,发布事件时。

图 8-23。 在将事件发布到与辅助 microservice 事件总线原子性

为简单起见,eShopOnContainers 示例加事件总线上使用 (没有其他进程或检查器微服务) 的第一种方法。 但是,eShopOnContainers 不处理所有可能的失败情况。 在实际的应用程序部署到云,你必须接受最终,将出现问题,则必须实现,检查和重新发送逻辑的事实。 如果你有该表作为单个源的事件的事件总线通过发布它们时,使用作为队列的表可能会比第一种方法更有效。

发布通过事件总线集成事件时实现原子性

下面的代码演示如何创建一个事务涉及多个 DbContext 对象 — 一个与正在更新的原始数据相关的上下文和 IntegrationEventLog 表相关的第二个上下文。

请注意,下面的示例代码中的事务将不会有弹性,如果连接到数据库时运行代码时有任何问题。 这会在基于云的系统,如 Azure SQL DB,可能会在服务器之间移动数据库。 有关跨多个上下文中实现弹性事务,请参阅实现弹性 Entity Framework 核心 SQL 连接在本指南后面的部分。

为清楚起见,下面的示例演示在一段单独的代码中的整个过程。 但是,eShopOnContainers 实现实际重构,并将此逻辑拆分为多个类,因此很易于维护。

// Update Product from the Catalog microservice
//
public async Task<IActionResult>
    UpdateProduct([FromBody]CatalogItem productToUpdate)
{
    var catalogItem = await _catalogContext.CatalogItems
        .SingleOrDefaultAsync(i => i.Id == productToUpdate.Id);

    if (catalogItem == null) return NotFound();

    bool raiseProductPriceChangedEvent = false;

    IntegrationEvent priceChangedEvent = null;

    if (catalogItem.Price != productToUpdate.Price)
        raiseProductPriceChangedEvent = true;

    if (raiseProductPriceChangedEvent) // Create event if price has changed
    {
        var oldPrice = catalogItem.Price;
        priceChangedEvent = new ProductPriceChangedIntegrationEvent(catalogItem.Id,
            productToUpdate.Price,
            oldPrice);
    }

    // Update current product
    catalogItem = productToUpdate;
    // Achieving atomicity between original DB and the IntegrationEventLog
    // with a local transaction

    using (var transaction = _catalogContext.Database.BeginTransaction())
    {
        _catalogContext.CatalogItems.Update(catalogItem);

        await _catalogContext.SaveChangesAsync();

        // Save to EventLog only if product price changed
        if(raiseProductPriceChangedEvent)
            await _integrationEventLogService.SaveEventAsync(priceChangedEvent);
        transaction.Commit();
   }

   // Publish to event bus only if product price changed

   if (raiseProductPriceChangedEvent)
   {
       _eventBus.Publish(priceChangedEvent);
       integrationEventLogService.MarkEventAsPublishedAsync(
           priceChangedEvent);
   }

   return Ok();
}

创建 ProductPriceChangedIntegrationEvent 集成事件后,将存储原始域操作 (更新的目录项) 的事务事件日志表中还包括事件的持久性。 这样,可以单个事务,而且你将始终能够检查是否已发送事件消息。

使用原始数据库操作时,使用针对该数据库的本地事务以原子方式更新事件日志表。 如果任一操作失败,将引发异常,并且事务能够回滚任何已完成的操作,因此维护域操作和发送的事件消息之间的一致性。

从订阅接收消息: 在接收方微服务中的事件处理程序

除了事件订阅逻辑,你需要实现 (如回调方法) 的集成事件处理程序的内部代码。 事件处理程序可以指定将接收和处理特定类型的事件消息。

事件处理程序首先从事件总线接收事件实例。 然后它将定位要处理的组件与该集成事件,传播和在接收方微服务的状态的更改作为保持事件相关。 例如,如果 ProductPriceChanged 事件起源于目录微服务,它在购物篮微服务中处理,并更改以及,此接收方购物篮微服务中的状态,如下面的代码中所示。

namespace Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling
{
    public class ProductPriceChangedIntegrationEventHandler :
        IIntegrationEventHandler<ProductPriceChangedIntegrationEvent>
    {
        private readonly IBasketRepository _repository;

        public ProductPriceChangedIntegrationEventHandler(
            IBasketRepository repository)
        {
            _repository = repository;
        }

        public async Task Handle(ProductPriceChangedIntegrationEvent @event)
        {
            var userIds = await _repository.GetUsers();
            foreach (var id in userIds)
            {
                var basket = await _repository.GetBasket(id);
                await UpdatePriceInBasketItems(@event.ProductId, @event.NewPrice, basket);
            }
        }

        private async Task UpdatePriceInBasketItems(int productId, decimal newPrice,
            CustomerBasket basket)
        {
            var itemsToUpdate = basket?.Items?.Where(x => int.Parse(x.ProductId) ==
                productId).ToList();
            if (itemsToUpdate != null)
            {
                foreach (var item in itemsToUpdate)
                {
                    if(item.UnitPrice != newPrice)
                    {
                        var originalPrice = item.UnitPrice;
                        item.UnitPrice = newPrice;
                        item.OldUnitPrice = originalPrice;
                    }
                }
                await _repository.UpdateBasket(basket);
            }
        }
    }
}

事件处理程序需要验证中的任何购物篮实例是否存在产品。 它还会更新每个相关的购物篮行项的项价格。 最后,它会创建警报以指示要显示给用户有关此价格更改,请在图 8-24 中所示。

图 8-24。 通过集成事件进行通信时,显示在购物篮中项价格更改

更新消息事件中的幂等性

更新消息事件的一个重要方面是在通信中的任何位置的失败会导致重试的消息。 否则后台任务可能会尝试发布已发布,创建了争用条件的事件。 你需要确保更新是幂等或它们提供足够的信息来确保你可以检测到有重复,放弃它,并发送回只有一个响应。

如前文所述,幂等性意味着操作可以执行多次而无需更改结果。 在消息传递环境中,如当通讯事件,事件是幂等的如果就可以将它传递多次而无需更改接收方微服务构成的结果。 这可能是必要的事件本身,性质,因此,或由于系统处理事件。 在使用消息传递任何应用程序,而不仅仅是在应用程序中实现事件总线模式重要消息幂等性。

幂等操作的示例是将数据插入到表中,仅当该数据已不在表中的 SQL 语句。 它并不重要多少次你运行插入 SQL 语句;结果将是相同-表将包含该数据。 如果可能无法发送消息处理消息时的需要和因此处理的超过一次,也可以是幂等性如下。 例如,如果重试逻辑导致发件人将完全相同的消息发送多次,你需要确保它是幂等。

很可能设计幂等消息。 例如,可以创建事件,指出”设置为产品价格$25”而不是”添加$5 到产品价格。” 你无法安全地处理第一条消息任意次数,结果将相同。 不适用于第二条消息。 但是,即使在第一种情况,你可能不想处理的第一个事件,因为系统无法也发送的较新的价格更改事件,并且你将覆盖新价格。

另一个示例可能是未传播至多个订阅服务器的顺序完成事件。 很重要,订单信息更新在其他系统中只需一次,即使没有为相同的顺序完成事件重复的消息事件。

它很方便地具有某种类型的每个事件的标识,以便您可以创建强制实施的接收方每一次处理每个事件的逻辑。

某些消息处理本质上是幂等。 例如,如果系统生成的缩略图,也不可能会影响此多少次处理有关生成缩略图的消息;结果是生成缩略图,并且它们基本相同,每次。 另一方面,例如在调用支付网关,要收取信用卡号的操作可能不在所有是幂等。 在这些情况下,你需要确保处理多次的消息具有预期的效果。

消除重复集成事件消息

你可以确保发送和每个不同级别的订阅服务器只需一次处理的消息事件。 一种方法是使用提供的使用消息传递基础结构的重复数据删除功能。 另一种是在目标微服务中实现自定义逻辑。 在传输级别和应用程序级别具有验证是最佳选择。

消除重复消息 EventHandler 级事件

请确保将由任何接收方只需一次处理事件的一种方法是通过实现某些逻辑处理事件处理程序中的消息事件时。 例如,这是 eShopOnContainers 应用程序中使用的方法在中可以看到源代码OrdersController 类在接收 CreateOrderCommand 命令时。 (在这种情况下,我们使用的 HTTP 请求命令,而不是基于消息的命令,但你需要进行基于消息的命令幂等的逻辑是类似。)

消除重复的消息时使用 RabbitMQ

间歇性网络故障发生时,可以重复消息,并且消息接收方必须准备好以处理这些重复的消息。 如果可能,接收方应处理以幂等方式,即显式使用重复数据删除处理它们更好的消息。

根据RabbitMQ 文档,”如果一条消息是发送到使用者,然后重新排队,(因为它无法确认之前使用者断开连接,例如) 则 RabbitMQ 将对设置重新发送的标志它是否为同一使用者或另一个) 再次传递时。

如果已设置的”重新发送”标志,接收方必须考虑的帐户,因为消息可能已处理。 但是,不能保证;消息可能永远不会已达到接收方后它保持消息代理中,可能是由于网络问题。 另一方面,如果未设置”重新发送”标志,因而可以保证,消息未发送一次以上。 因此,接收方需要进行重复数据删除以幂等方式处理消息仅当”重新发送”标志设置消息中。

阅读更多
换一批

没有更多推荐了,返回首页