1. 引言

事件总线解决了微服务间如何基于集成事件进行异步通信的问题。然而只有事件总线正常运行,微服务之间基于事件的通信才得以运转。

而现实情况是,总有这样或那样的问题,导致事件总线不稳定或不可用,比如:网络中断,系统断电等等,这都可能导致微服务间的不一致性问题。

那如何解决事件总线故障导致的不一致问题呢?

  1. 事件溯源
  2. 事件日志挖掘
  3. 发件箱模式

2. 问题

既然上面提到了一致性问题,那具体的问题是什么呢,在什么情况才会发生呢?我想我有必要简单举例。上代码:

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);

当产品价格更改后,代码将数据提交给数据库,然后发布ProductPriceChangedIntegrationEvent 事件。

如果服务在数据库更新后崩溃(奔溃发生在_context.SaveChangesAsync()代码执行之后,但又发生在集成事件成功发布前),就会导致本地微服务价格已成功更新,但集成事件未发布的问题。就会导致目录微服务中定义的价格和顾客购物车中缓存的价格不一致。

3. 分析问题

以上问题的关键在于是如何确保两个独立的操作的原子性。如果单从单体应用的角度来处理的话,我们完全是可以将他们放到同一个事务中去保证。然而在微服务中,就违背了其高可用的基本要求。因为一旦事件总线处于瘫痪状态,那么整个目录微服务就不可用了。这种强制通过事务保证的一致性,就引入了太多的问题依赖。

如果从微服务的角度来看,每个微服务负责各自的业务逻辑,对于目录微服务来说,它的关注点是产品的更新是否成功。至于借助事件总线通过异步事件实现微服务间的通信,并不是其关注点。这也就是关注点分离。换句话说,产品的更新不应该依赖外部状态。在这里,外部状态就是事件总线的可用性。

你可能会说了,既然不允许通过强事务保证一致性,那么如何解决一致性问题呢(好像绕了半天又回到了原点)?

这里就要引入强一致性和最终一致性的概念了。

强一致性:也就是事务一致性,将多个操作放到单一事务处理。要么全部成功,要么全部失败。



最终一致性:通过将某些操作的执行延迟到稍后的时间来执行。若前面的操作执行成功,后续操作将延后执行。若前面的操作失败,后续的操作就不会执行。

到这里,我们实际要解决的问题就明确了:如何确保事件总线能够正确进行事件转发?

换句话说:事件总线挂了,但是事件消息不能丢失。只要事件消息不丢,后面我们还有机会挽救(重新发布消息)。

如何保证事件消息不丢失呢?当然是持久化了。

4. 持久化事件源

eShopOnContainers已经考虑了这一点,集成了事件日志用于持久化。我们直接来看类图:



从类图中看其实现逻辑也很简单,主要是定义了一个IntegrationEventLogEntry实体、EventStateEnum事件状态枚举和IntegrationEventLogContextEF上下文用于事件日志持久化。暴露IIntegrationEventLogService用于事件状态的更新。

其他微服务通过在启动类中注册IntegrationEventLogContext即可完成事件日志的集成。

services.AddDbContext<IntegrationEventLogContext>(options =>
{
options.UseSqlServer(configuration["ConnectionString"],
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(typeof(Startup)
.GetTypeInfo().Assembly.GetName().Name);
sqlOptions.EnableRetryOnFailure(maxRetryCount: 10,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
});
});

使用EF进行数据库迁移后,就会生成IntergrationEventLog表。如下图所示:

5. 如何借助事件日志确保高可用

主要分两步走:

  1. 应用程序开始本地数据库事务,然后更新领域实体状态,并将集成事件插入集成事件日志表中,最后提交事务来确保领域实体更新和保存事件日志所需的原子性。
  2. 发布事件

第一步毋庸置疑,第二步发布事件,我们又有多种实现方式:

  1. 在提交事务后立即发布集成事件,并将其标记为已发布。当微服务发生故障时,可以通过遍历存储的集成事件(未发布)执行补救措施。
  2. 将事件日志表用作一种队列。使用单独的线程或进程查询事件日志表,将事件发布到事件总

    线,然后将事件标记为已发布。

这里很显然第二种方式更为稳妥。而eShopOnContainers出于简单考虑,采用了第一种方案,具体代码如下:

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 the intergation event through the event bus
_eventBus.Publish(priceChangedEvent);
integrationEventLogService.MarkEventAsPublishedAsync( priceChangedEvent);

至此,eShopOnContainers确保事件总线能够正确转发消息的解决方案阐述完毕。你可能会问,这对应的是引言中的哪一种方案?都不是,你可以看作其是基于事件日志的简化版的事件溯源。

6. 仅此而已?

通过持久化事件日志来避免事件发布失败导致的一致性问题,是一种有效措施。然而消息从发送到接收再到正常消费的过程中,每一个环节都可能故障,所以仅仅在消息发送端使用事件日志只是确保最终一致性的一小步。还有很多问题有待完善:

  1. 消息发送成功了,但未被成功接收
  2. 消息发送且成功接收,但未被正确消费
  3. 消息重复发送,导致多次消费问题
  4. 消息被多个微服务订阅,如何确保每个微服务都成功接收并消费
  5. 等等

而这些问题就留给大家思考吧。

eShopOnContainers 知多少[6]:持久化事件日志的更多相关文章

  1. eShopOnContainers 知多少[1]:总体概览

    引言 在微服务大行其道的今天,Java阵营的Spring Boot.Spring Cloud.Dubbo微服务框架可谓是风水水起,也不得不感慨Java的生态圈的火爆.反观国内.NET阵营,微服务却不愠 ...

  2. 【第一章】zabbix3.4监控WindowsCPU使用率磁盘IO磁盘事件日志监控阈值邮件报警详细配置

    Windows安装zabbix-agent 监控Windows-CPU使用率 监控Windows-磁盘IO性能监控 监控Windows/Linux-磁盘触发器阈值更改 监控Windows-网卡自动发现 ...

  3. 其他信息: 未找到源,不过,未能搜索部分或所有事件日志。 若要创建源,您需要用于读取所有事件日志的权限以确保新的源名称是唯一的。 不可访问的日志: Security。

    其他信息: 未找到源,不过,未能搜索部分或所有事件日志.  若要创建源,您需要用于读取所有事件日志的权限以确保新的源名称是唯一的.  不可访问的日志: Security. System.Diagnos ...

  4. Windows服务安装异常:System.Security.SecurityException: 未找到源,但未能搜索某些或全部事件日志。不可 访问的日志: Security

    Windows服务安装异常:System.Security.SecurityException: 未找到源,但未能搜索某些或全部事件日志.不可 访问的日志: Security 2种方法处理: 一.右键 ...

  5. Spring AOP 实现写事件日志功能

    什么是AOP?AOP使用场景?AOP相关概念?Spring AOP组件?如何使用Spring AOP?等等这些问题请参考博文:Spring AOP 实现原理 下面重点介绍如何写事件日志功能,把日志保存 ...

  6. 使用EventLog类写Windows事件日志

    在程序中经常需要将指定的信息(包括异常信息和正常处理信息)写到日志中.在C#3.0中可以使用EventLog类将各种信息直接写入Windows日志.EventLog类在System.Diagnosti ...

  7. EventLog实现事件日志操作

    选中”我的电脑”,在其右键菜单中选择“管理”,在打开的对话框中包括了如下图所示的“日志”信息: 选中其中的某一条日志,可以看到如下的详细信息: 我们应该如何通过写代码的方式向其中添加“日志”呢? 在操 ...

  8. 事件日志:无法加载站点/服务的所有 ISAPI 筛选器。因此启动中止。

    事件日志:无法加载站点/服务的所有 ISAPI 筛选器.因此启动中止. Service Unavailable解决 故障状态:Internet 信息服务(IIS)管理器 里 应用程序池出现错误 “应用 ...

  9. .NET拾忆:EventLog(Windows事件日志监控)

    操作Windows日志:EventLog 1:事件日志名(logName):“事件查看器”中的每一项,如“应用程序”.“Internet Explorer”.“安全性”和“系统”都是日志(严格地说是日 ...

随机推荐

  1. (linux虚拟机)克隆得到的虚拟机修改网卡信息和IP地址,以及DNS

    克隆得到的虚拟机,与原先的系统是一模一样的包括MAC地址和IP地址.需要修改成信息. 克隆完事之后,首先在 点击生成一个新的MAC地址.然后启动,登陆. vim /etc/udev/rules.d/7 ...

  2. Interface Development

  3. DDGScreenShot—截取图片的任意部分

    写在前面 DDGScreenShot 库提供了截取任意图片的功能, 支持手势截图,当然,输入任意的区域也可以,下面看看具体的代码 代码如下: 方法封装 /** ** 用手势截图(截取图片的任意部分) ...

  4. 搭建centos7的开发环境1-系统安装及Python配置

    在公司开发部干活的好处是可以再分配一台高性能的PC,有了新的工作电脑,原来分配的笔记本电脑就可以安装linux系统做开发了,主要有两方面的开发计划,一个是计划中要参与爬虫系统的开发,第二个是大数据环境 ...

  5. SOFA 源码分析 — 负载均衡和一致性 Hash

    前言 SOFA 内置负载均衡,支持 5 种负载均衡算法,随机(默认算法),本地优先,轮询算法,一致性 hash,按权重负载轮询(不推荐,已被标注废弃). 一起看看他们的实现(重点还是一致性 hash) ...

  6. Oracle12c中多宿主容器数据库(CDBs)和可插拔数据库(PDBs)新特性之运行脚本

    对开发者和DBA们来说,对shell脚本批量任务的影响成了多宿主选项带来的最大改变之一.因为多宿主环境通过服务来连接到可插拔数据库,因此,依靠CRON和OS认证成了换成多宿主环境后的一个最大问题.本文 ...

  7. 30岁天才上班族利用Python人脸监控BOSS,伪装成认真上班的样子!

    如今Python程序员可以做深度学习算法实现人脸识别,得益于国外开源框架,虽然它不能达到face++和众多人脸识别公司,但实际应用并没有受到太大的压力.下图为tensorflow的5点定位加情感测试. ...

  8. jvm GC

    JavaGC.新生代.老年代 Java 中的堆是 JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象. 在 Java 中,堆被划分成两个不同的区域:新生代 ( Young ).老年代 ( ...

  9. MySQL触发器在PHP项目中用来做信息备份、恢复和清空的方法介绍

    案例:通过PHP后台代码可以将员工的信息删除,将删除的员工信息进行恢复(类似于从回收站中恢复员工信息),并且还可以将已经删除的员工进行清空(类似于清空回复站的功能). 思路:要有一张员工表,还要有一张 ...

  10. display的属性值测试

    由于在学习CSS的display的属性值只针对block.inline.inline-block和flex进行过了解,并且自己观察得知列表中li的display属性是list-item,而想要触发BF ...