Concurrency DDD-并发和命令重试,但有副作用

Concurrency DDD-并发和命令重试,但有副作用,concurrency,domain-driven-design,cqrs,event-sourcing,Concurrency,Domain Driven Design,Cqrs,Event Sourcing,我正在开发一个事件源电动汽车充电站管理系统,该系统连接到多个充电站。在这个领域,我为充电站提供了一个聚合,其中包括充电站的内部状态(如果汽车正在使用充电站的连接器充电,则充电站是否已连接到网络) 站点通过标准化协议中定义的消息通知我其状态: 心跳:站点是否仍然“活动” StatusNotification:站点是否遇到错误(电压过低),或者是否一切正常 我的服务器可以向该站点发送命令: RemoteStartTransaction:通知电台解锁并保留其中一个连接器,以便汽车使用该连接器充电

我正在开发一个事件源电动汽车充电站管理系统,该系统连接到多个充电站。在这个领域,我为充电站提供了一个聚合,其中包括充电站的内部状态(如果汽车正在使用充电站的连接器充电,则充电站是否已连接到网络)

站点通过标准化协议中定义的消息通知我其状态:

  • 心跳:站点是否仍然“活动”
  • StatusNotification:站点是否遇到错误(电压过低),或者是否一切正常
我的服务器可以向该站点发送命令:

  • RemoteStartTransaction:通知电台解锁并保留其中一个连接器,以便汽车使用该连接器充电
我已经为这个充电站开发了一个聚合。它包含其连接器的内部实体,无论是否正在充电,如果电源系统有问题

聚合的内存表示驻留在我控制的服务器中,而不是充电站本身,它有一个
StationClient
服务,负责将这些命令发送到物理充电站(伪代码):


我使用的是乐观并发,这意味着,我从事件列表加载聚合,并将聚合的当前版本存储在其内存表示中:例如,在版本2032中使用StationAggregate,当成功处理命令并应用事件时,它将在版本2033中使用。这样,我就可以在持久化层的(StationID,Version)元组上设置一个惟一的约束,并保证只持久化一个事件

如果有可能,会出现心跳消息的接收和解锁命令的接收。在这两个线程中,它们都将加载StationAggregate,并且都在版本X中,在心跳接收器的情况下,不会有副作用,但在解锁命令的情况下,会有副作用,告诉物理充电站要解锁。然而,当我使用乐观并发时,
StationUnlocked
事件可能会被持久层拒绝。我不知道如何处理这个问题,因为我无法重试该命令,因为它本质上不是幂等的(因为物理站会拒绝第二个请求)


我不知道我是在建模错误,还是真的很难建模。

我不确定我是否完全理解这个问题,但乐观并发的思想是防止在竞争条件下写入。版本用于确保在执行命令之前,写入操作的版本与从数据库获得的版本相差+1

因此,如果有一个并行写入成功,并且您从事件存储中返回了错误的版本异常,那么您将完全重试命令执行,这意味着您将再次读取流,通过这样做,您将获得新版本的最新状态。然后,将命令交给聚合,聚合决定执行操作是否合理

这个问题与事件源并不特别相关,它与任何持久性都同样相关,并且以相同的方式解决

事件来源可以为您带来额外的好处,因为您知道发生了什么。想象一下,您意外地获得了两次
Unlock
命令。当您从存储区返回“错误版本”时,您可以读取最后一个事件并确定命令是否已执行。可以从逻辑上(如果同一客户已经解锁,则无需解锁)、技术上(将命令id放入事件元数据并进行比较)或者两种方式都可以完成

在处理重复的命令时,有必要确保命令处理的幂等性,忽略重复的命令并返回OK,而不是失败到用户的脸上

另一个观察结果是,我可以从关于这个领域的非常有限的信息中推断出,心跳是遥测的,锁定和解锁是业务。我认为在一个域对象中组合这两个截然不同的东西没有多大意义

更新,在评论中讨论之后:

在生成事件的同时将命令发送到工作站所得到的是两阶段提交的变化。因为它不是在事务中执行的,所以这两个操作中的任何一个都可能失败,并导致系统处于不一致的状态。如果命令发送失败,您不知道站点是否获得了解锁自身的命令,或者如果事件持久化失败,您不知道它是否已解锁。你只做了第二次手术,但第一次也可能发生

有很多方法可以解决这个问题

首先,解决它完全是技术性的。有了MassTransit,使用。在原始消息的使用者完全完成其工作之前,它不会发送任何传出消息。因此,如果
Unlock
命令的使用者未能持久化事件,则不会发送该命令。然后,重试过滤器将接合,整个操作将再次执行,并且您已经退出竞争条件,因此操作将正确完成

但是,当您对物理站的命令无法发送时,它无法解决问题(我认为这是一种边缘情况)

这个问题也可以很容易地解决,在这里事件源是有帮助的。您需要将命令从原始(用户驱动)命令使用者发送到站点的转换为订阅服务器。您订阅
StationUnlocked
事件的事件流,并让订阅者向
class StationAggregate {
  stationClient: StationClient
  URL: string
  connector: Connector[]

  unlock(connectorId) {
    if this.connectors.find(connectorId).isAvailableToBeUnlocked() {
      return ErrorConnectorNotAvailable
    }
    error = this.stationClient.sendRemoteStartTransaction(this.URL, connectorId)
    if error {
      return ErrorStationRejectedUnlock
    }
    this.applyEvents([
      StationUnlockedEvent(connectorId, now())
    ])
    return Ok
  }

  receiveHeartbeat(timestamp) {
    this.applyEvents([
      StationSentHeartbeat(timestamp)
    ])
    return Ok
  }
}