该问题假定使用事件采购。
通过重放事件重建当前状态时,事件处理程序应该是幂等的。例如,当用户成功更新其用户名时,可能会发出UsernameUpdated
事件,该事件包含newUsername
字符串属性。重建当前状态时,相应的事件处理程序会收到UsernameUpdated
事件,并将username
对象上的User
属性设置为newUsername
事件的UsernameUpdated
属性宾语。换句话说,多次处理同一个消息总会产生相同的结果。
但是,在与外部服务集成时,这样的事件处理程序如何工作?例如,如果用户想要重置其密码,User
对象可能会发出PasswordResetRequested
事件,该事件由发出第三方的代码的一部分处理,该命令用于发送SMS。现在,当重建应用程序时,我们不想重新发送此SMS。如何最好地避免这种情况?
答案 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框架文档,以便更好地了解问题及其解决方案。
答案 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可能发生冲突的可能性非常低,但实际上可能会发生,这可能会导致多个问题。
阻止与外部服务的通信。例如,如果用户" bob"请求密码重置,生成短信UUID为" 1234",然后(也许2年后)用户" frank"请求密码重置,生成相同的短信UUID" 1234",进程管理器不会将SMS排队,因为它认为它已经存在,所以坦率地永远不会看到它。
读取模型中的报告不正确。因为存在重复的UUID,所以读取侧可以显示发送到" bob"当" frank"正在查看系统发送给他的短信列表。如果重复的UUID是快速连续生成的,则有可能" frank"能够重置" bob的密码。
其次,将SMS UUID生成移动到事件中意味着您必须让User
汇总知道PasswordResetProcessManager
的功能(但不是{ {1}}本身),它增加了耦合。但是,此处的耦合是松散的,因为PasswordResetManager
不知道如何对SMS进行排队,只有SMS应该排队。如果User
类要自己发送短信,则可能会遇到User
事件存储而SmsQueued
事件未存在的情况,这意味着用户将收到SMS但生成的密码重置代码未保存在用户身上,因此输入代码不会重置密码。
第三,如果生成PasswordResetRequested
事件但系统在PasswordResetRequested
创建SMS之前崩溃,则最终将发送SMS,但仅在PasswordResetProcessManager
事件发生时重播(这可能是将来很长一段时间)。例如,"最终"最终一致性的一部分可能需要很长时间。
上述方法有效(我可以看到它也应该在更复杂的情况下工作,例如此处描述的PasswordResetRequested
:https://msdn.microsoft.com/en-us/library/jj591569.aspx)。但是,我非常希望听到其他人对这种方法的看法。