1

该问题假设使用事件溯源。

通过重放事件重建当前状态时,事件处理程序应该是幂等的。例如,当用户成功更新其用户名时,UsernameUpdated可能会发出一个事件,该事件包含一个newUsername字符串属性。重建当前状态时,适当的事件处理程序接收UsernameUpdated事件并将对象上的username属性设置为事件对象的属性。换句话说,多次处理相同的消息总是产生相同的结果。UsernewUsernameUsernameUpdated

但是,当与外部服务集成时,这样的事件处理程序是如何工作的呢?例如,如果用户想要重置他们的密码,该User对象可能会发出一个PasswordResetRequested事件,该事件由一部分代码处理,该部分代码向第 3 方发出发送 SMS 的命令。现在,当应用程序重新构建时,我们不想重新发送此 SMS。如何最好地避免这种情况?

4

3 回答 3

2

交互中涉及两个消息:命令和事件。

我不认为消息传递基础架构中的系统消息与域事件相同。命令消息处理应该是幂等的。事件处理程序通常不需要。

在您的场景中,我可以告诉聚合根 100 次来更新用户名:

public UserNameChanged ChangeUserName(string username, IServiceBus serviceBus)
{
    if (_username.Equals(username))
    {
        return null;
    }

    serviceBus.Send(new SendEMailCommand(*data*));

    return On(new UserNameChanged{ Username = userName});
}

public UserNameChanged On(UserNameChanged @event)
{
    _username = @event.UserName;

    return @event;
}

上面的代码会产生一个事件,因此重构它不会产生任何重复的处理。即使我们有 100 个UserNameChanged事件,结果仍然是相同的,因为该On方法不执行任何处理。我想要记住的一点是命令端完成所有实际工作,而事件端用于更改对象的状态。

以上不一定是我将如何实现消息传递,但它确实展示了这个概念。

于 2015-12-14T06:25:56.933 回答
0

TLDR;将 SMS 标识符存储在事件本身中。

事件溯源的一个核心原则是“幂等性”。事件是幂等的,这意味着多次处理它们将产生与处理一次相同的结果。命令是“非幂等的”,这意味着命令的重新执行对于每次执行可能有不同的结果

聚合由 UUID 标识(重复率非常低)这一事实意味着客户端可以生成新创建的聚合的 UUID。流程管理器(又名“Sagas”)通过侦听事件发出命令来协调跨多个聚合的操作,因此从这个意义上说,流程管理器也是一个“客户端”。由于进程管理器发出命令,它不能被认为是“幂等的”。

我想出的一种解决方案是在PasswordResetRequested事件中包含即将创建的 SMS 的 UUID。这允许流程管理器仅在 SMS 尚不存在时才创建它,从而实现幂等性。

下面的示例代码(C++ 伪代码):


// The event indicating a password reset was successfully requested.
class PasswordResetRequested : public Event {
public:
    PasswordResetRequested(const Uuid& userUuid, const Uuid& smsUuid, const std::string& passwordResetCode);

    const Uuid userUuid;
    const Uuid smsUuid;
    const std::string passwordResetCode;
};

// The user aggregate root.
class User {
public:

    PasswordResetRequested requestPasswordReset() {
        // Realistically, the password reset functionality would have it's own class 
        // with functionality like checking request timestamps, generationg of the random
        // code, etc.
        Uuid smsUuid = Uuid::random();
        passwordResetCode_ = generateRandomString();
        return PasswordResetRequested(userUuid_, smsUuid, passwordResetCode_);
    }

private:

    Uuid userUuid_;
    string passwordResetCode_;

};

// The process manager (aka, "saga") for handling password resets.
class PasswordResetProcessManager {
public:

    void on(const PasswordResetRequested& event) {
        if (!smsRepository_.hasSms(event.smsUuid)) {
            smsRepository_.queueSms(event.smsUuid, "Your password reset code is: " + event.passwordResetCode);
        }
    }

};

关于上述解决方案,有几点需要注意:

首先,虽然 SMS UUID 发生冲突的可能性(非常)低,但它实际上可能会发生,这可能会导致几个问题。

  1. 与外部服务的通信被阻止。例如,如果用户“bob”请求重置密码并生成“1234”的 SMS UUID,那么(可能 2 年后)用户“frank”请求生成相同的 SMS UUID“1234”的密码重置,该过程经理不会排队短信,因为它认为它已经存在,所以弗兰克永远不会看到它。

  2. 读取模型中的错误报告。因为存在重复的 UUID,所以当“frank”正在查看系统发送给他的 SMSes 列表时,读取端可能会显示发送给“bob”的 SMS。如果重复的 UUID 是连续快速生成的,“frank”可能能够重置“bob”的密码。

其次,将 SMS UUID 生成移动到事件中意味着您必须让User聚合了解 的PasswordResetProcessManager功能而不是其PasswordResetManager本身),这会增加耦合。然而,这里的耦合是松散的,因为User不知道如何排队 SMS,只知道 SMS 应该排队。如果User类自己发送 SMS,您可能会遇到SmsQueued事件被存储而PasswordResetRequested事件未存储的情况,这意味着用户将收到 SMS 但生成的密码重置代码未保存在用户身上,并且所以输入密码不会重置密码。

第三,如果一个PasswordResetRequested事件产生了,但系统在PasswordResetProcessManager可以创建短信之前就崩溃了,那么短信最终会被发送,但只有在PasswordResetRequested事件被重新播放时(这可能是未来很长一段时间)。例如,最终一致性的“最终”部分可能需要很长时间。


上述方法有效(我可以看到它也应该在更复杂的场景中工作,就像OrderProcessManager这里描述的:https ://msdn.microsoft.com/en-us/library/jj591569.aspx )。但是,我很想听听其他人对这种方法的看法。

于 2015-12-14T22:49:46.847 回答
0

我认为您在这里混合了两个独立的概念。第一个是重构一个对象,其中处理程序都是实体本身的内部方法。Axon 框架的示例代码

public class MyAggregateRoot extends AbstractAnnotatedAggregateRoot {

@AggregateIdentifier
private String aggregateIdentifier;
private String someProperty;

public MyAggregateRoot(String id) {
    apply(new MyAggregateCreatedEvent(id));
}

// constructor needed for reconstruction
protected MyAggregateRoot() {
}

@EventSourcingHandler
private void handleMyAggregateCreatedEvent(MyAggregateCreatedEvent event) {
    // make sure identifier is always initialized properly
    this.aggregateIdentifier = event.getMyAggregateIdentifier();
    // do something with someProperty
}

}

当然,您不会将与外部 API 对话的代码放在聚合的方法中。

第二个是在有界上下文上重放事件,这可能会导致您正在谈论的问题,并且根据您的情况,您可能需要将事件处理程序划分为集群。

有关这一点,请参阅 Axon 框架文档,以更好地了解问题及其解决方案。

在集群上重放事件

于 2015-12-13T12:40:42.963 回答