TLDR;将 SMS 标识符存储在事件本身中。
事件溯源的核心原则是“幂等性”。事件是幂等的,这意味着多次处理它们将得到与处理一次相同的结果。命令是“非幂等的”,这意味着重新执行命令可能每次执行都有不同的结果。
聚合由 UUID 标识(重复率非常低)这一事实意味着客户端可以生成新创建的聚合的 UUID。流程管理器(又名“Sagas”)通过监听事件来协调多个聚合之间的操作,以便发出命令,因此从这个意义上说,流程管理器也是一个“客户”。因为进程管理器发出命令,所以不能认为是“幂等的”。
我想出的一个解决方案是将即将创建的 SMS 的 UUID 包含在 PasswordResetRequested 事件中。这允许流程管理器仅在 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”请求重置密码并生成“1234”的 SMS UUID,那么(可能 2 年后)用户“frank”请求生成相同的 SMS UUID“1234”的密码重置,该过程经理不会排队,因为它认为它已经存在,所以弗兰克永远不会看到它。
读取模型中的报告不正确。因为存在重复的 UUID,所以当“frank”正在查看系统发送给他的 SMSes 列表时,读取端可能会显示发送给“bob”的 SMS。如果连续快速生成重复的 UUID,“frank”可能会重置“bob”的密码。
其次,将 SMS UUID 生成移动到事件中意味着您必须让 User 聚合了解 PasswordResetProcessManager 的功能(但不是 PasswordResetManager 本身),这增加了耦合。但是,这里的耦合是松散的,因为User 不知道如何对 SMS 进行排队,只是应该对 SMS 进行排队。如果User类自己发送短信,你可能会遇到SmsQueued事件被存储而PasswordResetRequested事件不被存储的情况,这意味着用户将收到短信但生成的密码重置密码没有保存在用户身上,所以输入密码不会重置密码。
第三,如果产生了PasswordResetRequested事件,但在PasswordResetProcessManager可以创建短信之前系统崩溃了,那么短信最终会被发送,但只有当PasswordResetRequested事件被重新播放时(这可能未来很长一段时间)。例如,最终一致性的“最终”部分可能需要很长时间。
上述方法有效(我可以看到它也应该适用于更复杂的场景,例如此处描述的OrderProcessManager:https://msdn.microsoft.com/en-us/library/jj591569.aspx)。不过,我很想听听其他人对这种方法的看法。