实现事件订阅的C#部分

117 阅读4分钟

想象一下,我们有一个忠诚度计划的微服务。

图1显示了Loyalty Program微服务所参与的协作关系。忠诚度计划订阅了来自特别优惠的事件,它使用这些事件来决定何时通知注册用户新的特别优惠。


图1.Loyalty Program微服务中基于事件的协作是对Special Offers微服务中事件馈送的订阅。


我们首先看看Special Offers是如何在一个基于HTTP的feed中暴露其事件的。然后,我们将回到Loyalty Program,为该服务添加第二个进程,它将负责订阅事件和处理事件。

实现一个事件反馈

特别优惠 "微服务通过暴露一个端点--/events--来实现其事件反馈,该端点返回一个按顺序编号的事件列表。该端点可以接受两个查询参数--startend--指定一个事件范围。例如,对事件反馈的请求可以是这样的:

 GET /events?start=10&end=110 HTTP/1.1
  
 Host: localhost:5002
 Accept: application/json

对这个请求的响应可能如下,只是我在五个事件后切断了响应:

 HTTP/1.1 200 OK
 Content-Type: application/json; charset=utf-8
  
 [   {     "sequenceNumber": 1,     "occuredAt": "2020-06-16T20:13:53.6678934+00:00",     "name": "SpecialOfferCreated",     "content": {       "description": "Best deal ever!!!",       "id": 0     }   },   {     "sequenceNumber": 2,     "occuredAt": "2020-06-16T20:14:22.6229836+00:00",     "name": "SpecialOfferCreated",     "content": {       "description": "Special offer - just for you",       "id": 1     }   },   {     "sequenceNumber": 3,     "occuredAt": "2020-06-16T20:14:39.841415+00:00",     "name": "SpecialOfferCreated",     "content": {       "description": "Nice deal",       "id": 2     }   },   {     "sequenceNumber": 4,     "occuredAt": "2020-06-16T20:14:47.3420926+00:00",     "name": "SpecialOfferUpdated",     "content": {       "oldOffer": {         "description": "Nice deal",         "id": 2       },       "newOffer": {         "description": "Best deal ever - JUST GOT BETTER",         "id": 0       }     }   },   {     "sequenceNumber": 5,     "occuredAt": "2020-06-16T20:14:51.8986625+00:00",     "name": "SpecialOfferRemoved",     "content": {       "offer": {         "description": "Special offer - just for you",         "id": 1       }     }   } ]

注意,这些事件有不同的名字(SpecialOfferCreated,SpecialOfferUpdated, 和SpecialOfferRemoved ),不同类型的事件没有相同的数据字段。这很正常:不同的事件携带不同的信息。这也是你在实现Loyalty Program微服务中的订阅者时需要注意的问题。你不能期望所有的事件都有完全相同的形状。

特价服务中的/events端点的实现是一个简单的ASP.NET Core MVC控制器。

清单1.读取和返回事件的端点

 namespace SpecialOffers.Events
 {
   using System.Linq;
   using Microsoft.AspNetCore.Mvc;
  
   [Route(("/events"))]
   public class EventFeedController : Controller
   {
     private readonly IEventStore eventStore;
  
     public EventFeedController(IEventStore eventStore)
     {
       this.eventStore = eventStore;
     }
  
     [HttpGet("")]
     public ActionResult<EventFeedEvent[]> GetEvents([FromQuery] int start, [FromQuery] int end)
     {
       if (start < 0 || end < start)
         return BadRequest();
  
       return this.eventStore.GetEvents(start, end).ToArray();
     }
   }
 }

你可能会注意到,GetEvents 返回eventStore.GetEvents 的结果。ASP.NET Core将其序列化为一个数组。EventFeedEvent 是一个携带少量元数据的类,以及一个用于保存事件数据的Content 字段。

清单2.代表事件的Event

   public class EventFeedEvent
   {
     public long SequenceNumber { get; }
     public DateTimeOffset OccuredAt { get; }
     public string Name { get; }
     public object Content { get; }
  
     public EventFeedEvent(
       long sequenceNumber,
       DateTimeOffset occuredAt,
       string name,
       object content)
     {
       this.SequenceNumber = sequenceNumber;
       this.OccuredAt = occuredAt;
       this.Name = name;
       this.Content = content;
     }
   }

Content 属性用于事件的具体数据,也是SpecialOfferCreated 事件、SpecialOfferUpdatedSpecialOfferREmoved 事件之间的区别所在。在Content ,每一个都有自己的对象类型。

这就是暴露一个事件源的全部内容。这种简单性是使用基于HTTP的事件源来发布事件的巨大优势。基于事件的协作可以通过队列系统实现,但这又引入了另一项复杂的技术,你必须学会在生产中使用和管理。这种复杂性在某些情况下是有必要的,但肯定不是一直如此。

创建一个事件订阅者流程

订阅一个事件源本质上意味着你将轮询你所订阅的微服务的事件端点。每隔一段时间,你将向/events端点发送一个HTTPGET 请求,以检查是否有任何你尚未处理的事件。

我们将以两个主要部分来实现这种定期轮询:

  • 一个简单的控制台应用程序,读取一批事件
  • 我们将使用一个Kubernetes cron job来间隔运行控制台应用程序

把这两者放在一起,就实现了事件订阅。cron作业确保控制台应用程序以一定的时间间隔运行,每次控制台应用程序运行时,它都会发送HTTP GET请求,检查是否有任何事件需要处理。

实现事件订阅程序的第一步是用以下dotnet 命令创建一个控制台应用程序。

 PS> dotnet new console -n EventConsumer

并用dotnet 也运行它:

 PS> dotnet run

这个应用程序是空的,所以还没有什么有趣的事情发生,但在下一节中,我们将使它读取事件。

订阅一个事件源

你现在有了一个EventConsumer 控制台应用程序。它所要做的就是读取一批事件并跟踪下一批事件的起点。这可以通过以下方式完成。

清单3.从一个事件源读取一批事件

 using System;
 using System.IO;
 using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Text.Json;
 using System.Threading.Tasks;
  
 var start = await GetStartIdFromDatastore();                              

从数据库读取这批事件的起点。

向事件源发送 GET 请求。

❷从数据库中读取这批事件的起点。

调用方法来处理本批的事件。ProcessEvents 也更新 start 变量。

通过上面的代码,EventConsumer 可以读取一批事件,每次调用它都会读取下一批的事件。剩下的部分是处理这些事件。

清单4.反序列化,然后处理事件

 async Task ProcessEvents(Stream content)
 {
   var events =
     await JsonSerializer.DeserializeAsync<SpecialOfferEvent[]>(content)
     ?? new SpecialOfferEvent[0];
   foreach (var @event in events)
   {
     Console.WriteLine(@event);                                 

这里是处理事件的地方。

保持跟踪所处理的最高事件编号。

这里有几件事情需要注意:

  • 这个方法跟踪哪些事件已经被处理了 #2。这确保你不会从feed中请求你已经处理过的事件。
  • 我们将事件上的Content 属性视为dynamic #1。正如你在前面看到的,并不是所有的事件在Content 属性中都携带相同的数据,所以把它当作动态的允许你在.Content 上访问你需要的属性而不关心其他的。这是一个合理的方法,因为你想自由地接受传入的数据--如果特价服务决定在事件的JSON中增加一个额外的字段,它不应该造成问题。只要你需要的数据在那里,其余的就可以忽略不计。
  • 事件被反序列化为类型SpecialOfferEvent 。这是与用于序列化Special Offers中的事件的EventFeedEvent 类型不同的一种类型。这是故意的,因为这两个微服务不需要对事件有完全相同的看法。只要Loyalty Program不依赖不存在的数据,一切都很好。

这里使用的SpecialOfferEvent 类型很简单,只包含Loyalty Program中使用的字段。

 public record SpecialOfferEvent(
   long SequenceNumber,
   DateTimeOffset OccuredAt,
   string Name,
   object Content);

至此,你实现事件订阅的C#部分就结束了。