使用MDA时,您应该区分幂等和非幂等事件处理程序吗?

时间:2015-12-13 11:11:07

标签: distributed-computing event-sourcing event-driven-design

该问题假定使用事件采购。

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

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

3 个答案:

答案 0 :(得分: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方法不执行任何处理相同。我想要记住的是,命令端执行所有实际工作,而事件端仅使用 来更改对象的状态。

以上并不一定是我如何实现消息传递,但确实证明了这一概念。

答案 1 :(得分:0)

我认为你在这里混合了两个独立的概念。第一个是重建一个对象,其中处理程序是实体本身的所有内部方法。 Sample code from Axon framework

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框架文档,以便更好地了解问题及其解决方案。

Replaying Events on a Cluster

答案 2 :(得分:0)

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

事件采购的核心原则是" idempotency"。事件是幂等的,这意味着多次处理它们将具有与处理一次相同的结果。命令是"非幂等",意味着重新执行命令可能会对每次执行产生不同的结果

聚合由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"请求密码重置,生成短信UUID为" 1234",然后(也许2年后)用户" frank"请求密码重置,生成相同的短信UUID" 1234",进程管理器不会将SMS排队,因为它认为它已经存在,所以坦率地永远不会看到它。

  2. 读取模型中的报告不正确。因为存在重复的UUID,所以读取侧可以显示发送到" bob"当" frank"正在查看系统发送给他的短信列表。如果重复的UUID是快速连续生成的,则有可能" frank"能够重置" bob的密码。

  3. 其次,将SMS UUID生成移动到事件中意味着您必须让User汇总知道PasswordResetProcessManager功能(但不是{ {1}}本身),它增加了耦合。但是,此处的耦合是松散的,因为PasswordResetManager不知道如何对SMS进行排队,只有SMS应该排队。如果User类要自己发送短信,则可能会遇到User事件存储而SmsQueued事件未存在的情况,这意味着用户将收到SMS但生成的密码重置代码未保存在用户身上,因此输入代码不会重置密码。

    第三,如果生成PasswordResetRequested事件但系统在PasswordResetRequested创建SMS之前崩溃,则最终将发送SMS,但仅在PasswordResetProcessManager事件发生时重播(这可能是将来很长一段时间)。例如,"最终"最终一致性的一部分可能需要很长时间。

    上述方法有效(我可以看到它也应该在更复杂的情况下工作,例如此处描述的PasswordResetRequestedhttps://msdn.microsoft.com/en-us/library/jj591569.aspx)。但是,我非常希望听到其他人对这种方法的看法。