[关闭]
@xuemingdeng 2022-10-23T13:15:45.000000Z 字数 3870 阅读 245

事件驱动架构中最常见的4个错误,你曾经犯过吗

202210


作者丨The Bored Dev
译者丨明知山
策划丨杜小芳

在过去的几年中,大公司对事件驱动系统的采用有了相当大幅度的增长。这一趋势背后的主要原因是什么?这纯粹是炒作还是有任何有效的理由来支持采用这种架构?在我们看来,许多公司之所以走这条路,主要有以下几个原因。

组件之间的松散耦合

通过事件进行异步交互可以让组件实现低耦合。我们可以在不同的时间单独修改、部署和操作这些组件,就维护和生产力成本而言,这是一个巨大的好处。

触发并忘记

这个好处与松散耦合高度相关,但因为它比较重要,所以我们认为值得单独把它提出来说。这些系统最大的一个优点是可以触发事件,而我们并不关心如何以及何时处理这些事件,只关心它是否在相应的主题中持久存在。

一旦一种类型的事件被发送到一个主题,对这些事件感兴趣的新消费者就可以订阅并开始处理它们。生产者不需要做任何工作与新的消费者发生交互,因为生产者和消费者是完全解藕的。

在传统的同步HTTP通信中,生产者必须单独调用每一个消费者。因为对消费者的调用都可能存在失败的可能,所以实现和维护这种通信方式的成本要比事件驱动高得多。

没有时间耦合

其主要好处之一是组件不必同时都在运行。例如,其中一个组件可能不可用,但这完全不会影响到另一个组件(只要它有任务要处理)。

将一些业务模型融入基于事件的系统相对容易

有些模型,特别是那些实体在生命周期中经历不同阶段的模型,与基于事件的系统非常匹配。将模型定义为一组基于原因(cause)和作用(effect)的相互关联的关系,每个“原因”都源于一个事件,这个事件对系统中的特定实体甚至多个实体产生给定的“副作用”。

基于上述的这些好处,我们可以清楚地看到为什么这种架构近年来变得越来越流行。对于公司来说,敏捷和低成本维护非常重要。架构的选择对公司的绩效会产生相当大的影响。我们必须明白,在未来的几年里,许多公司将是技术驱动型的,这就是为什么我们的工作和架构决策对公司的成功来说如此重要。

话虽如此,我们还必须知道,构建事件驱动的系统并不是一项容易的任务。初学者经常会犯一些常见的错误,让我们来看看其中的一些。

4个常见的错误

在采用了异步操作的系统中,许多事情都可能出错。我们已经发现了一些错误,这些错误在尚未进入高级阶段的项目中很容易发现。

没有顺序保证

第一个常见的错误是忽略了对业务模型中给定实体的某些操作的顺序保证。

例如,假设我们有一个支付系统,其中每笔支付都会因为一系列特定事件经过不同的状态。我们可以有一个ProcessPayment事件、一个CompletePayment事件和一个RefundPayment事件,这些事件(在本例中也叫命令)中的每一个都将支付过程转换到相应的状态。

在这种情况下,如果我们不保证顺序会发生什么?我们可能会遇到这样的情况:对于同一笔支付,RefundPayment事件可能在CompletePayment事件之前被处理。这将意味着支付仍然是完成状态,尽管已经退了款。

之所以发生这种情况,是因为在处理退款事件时,支付处于不允许被退款的状态。我们可以采取其他措施来避免这个问题,尽管这样做并不高效。我们通过图表来说明这种情况,以便更好地理解这个问题!

此处输入图片的描述

在上图中,我们可以看到不同的消费者在消费具有相同支付ID的支付事件。这肯定会成为一个问题,主要是因为我们希望在处理同一笔支付的下一个事件之前等待上一个事件处理结束。

一些发布和订阅消息传递系统(如Kafka或Pulsar)提供了一种机制来轻松实现这一点。例如,在Pulsar中,你可以使用Key_Shared订阅模式来确保给定支付ID的事件总是由同一个消费者按顺序消费。在Kafka中,你可能不得不使用分区,所以要确保给定支付ID的所有事件被分配到相同的分区。

在新的场景中,属于同一个笔支付的所有事件将由相同的消费者处理。

此处输入图片的描述

非原子的多个操作

另一个常见的错误是在业务关键的地方执行了多个操作,并假定每个操作都是正确的。但请记住这一点——如果一件事可能失败,它就一定会失败。

例如,一个典型的场景是:持久化一个实体并在持久化实体之后发送一个事件。我们通过一个例子来更好地理解这个问题。

  1. class UserService(private val userRepository: UserRepository, private val eventsEngine: UserEventsEngine) {
  2.     fun create(user: User): User {
  3.         val savedUser = userRepository.save(user)
  4.         eventsEngine.send(UserCreated(UUID.randomUUID().toString(), user))
  5.         return User.create(savedUser)
  6.     }
  7. }

如果发送UserCreated事件失败会发生什么?此时用户数据将被持久化,我们的系统和消费者系统之间会出现不一致。有些人会说,如果你先发送UserCreated事件呢?那么,如果用户在发送事件后持久化失败会发生什么?

消费者会认为用户已经创建好了,但事实并非如此!

现在的主要问题是我们如何解决这个问题。我们有不同的方法可以解决这个问题,我们来看看其中的一些。

使用事务

解决这个不一致问题的一个简单方法是利用数据库的事务(如果数据库支持的话)。我们假设我们有一个withinTransaction方法,它将启动一个新的事务,并在事务失败时进行回滚。

  1. fun create(user: User): User {
  2.         return withinTransaction {
  3.             val savedUser = userRepository.save(user)
  4.             eventsEngine.send(UserCreated(UUID.randomUUID().toString(), user))
  5.             User.create(savedUser)
  6.         }
  7.     }

需要注意的是,在某些情况下,事务可能会对性能造成影响,所以在做任何决定之前请先看一下数据库文档。

发件箱模式

解决这个问题的另一种方法是在用户持久化后通过实现发件箱模式在后台发送事件。你可以在这里了解它是如何实现的。

此处输入图片的描述

发送多个事件

当我们试图在业务关键部分发送多个事件时,也会出现类似的问题。如果在发送了一些事件之后,再发送一个事件但发送失败,会发生什么情况?结果是不同的系统可能处于不一致的状态。

有一些方法可以避免这个问题,让我们来看看。

串联事件

解决这个问题的最好方法可能就是避免一次发送多个事件。你可以将事件串联起来,以便按顺序处理它们,而不是并行处理。

例如,我们来看一下这个场景。当一个用户被创建时,我们需要发送“注册成功”电子邮件。

  1. fun create(user: User): User {
  2.         return withinTransaction {
  3.             val savedUser = userRepository.save(user)
  4.             eventsEngine.send(UserCreated(UUID.randomUUID().toString(), user))
  5.             eventsEngine.send(SendRegistrationEmailEvent(UUID.randomUUID().toString(), user))
  6.             User.create(savedUser)
  7.         }
  8.     }

如果发送SendRegistrationEmailEvent失败,即使重试,也无法从错误中恢复。因此,注册邮件将永远不会被发送。如果我们改成这样做呢?

此处输入图片的描述

通过分离事件,在发送UserCreated事件后,其他消费者可以继续处理其他事件,同时将发送注册电子邮件的任务拆分成一个单独的功能,可以根据需要重试多次。

事务支持

如果你的消息传递系统支持事务,你还可以利用它们在出现错误时回滚所有事件。例如,Pulsar就支持事务。

不向后兼容的变更

我们要说的最后一个常见错误是,在修改现有事件时,没有考虑到事件必须向后兼容。

例如,假设我们正在向一个现有事件添加一些新字段并删除一个已有字段。

在下图中,我们可以分别看到发布变更的错误和正确的方法。

此处输入图片的描述

对于第一种情况,我们删除middleName字段并添加新的dateOfBirth字段。为什么这是有问题的?

第一个修改将会导致问题,并使现有的一些事件处于阻塞状态。为什么?

我们假设在进行新版本部署时,Users主题中已经有一些UserCreated事件。在不停机的情况下部署应用程序的最常见方法是所谓的滚动升级,因此,从现在开始,我们假定这就是部署应用程序的方法。

在部署过程中,可能有一些节点包含了新版本的代码变更,但仍然有一些节点运行旧版本的代码,因此不支持新版本的事件。

我们还必须记住,在第一次部署中发布的代码必须让新添加的字段是可选的,以便能够处理主题中已有的旧事件。在确定主题中没有旧事件后,我们就可以去掉这个限制,让它们变成非可选的。

结论

我们已经看到了事件驱动架构的好处。然而,要正确实现它并不是一件容易的事。因此,与具备这种架构经验的人进行结对编程尤其重要,因为这样可以为我们节省非常宝贵的时间。

如果你正在寻找一本更好地理解事件驱动系统的好书,我们强烈推荐“Building Event-Driven Microservices”。

原文链接:https://betterprogramming.pub/4-common-mistakes-in-event-driven-systems-9e70c06f79a9

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注