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 发生冲突的可能性(非常)低,但它实际上可能会发生,这可能会导致几个问题。
与外部服务的通信被阻止。例如,如果用户“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
类自己发送 SMS,您可能会遇到SmsQueued
事件被存储而PasswordResetRequested
事件未存储的情况,这意味着用户将收到 SMS 但生成的密码重置代码未保存在用户身上,并且所以输入密码不会重置密码。
第三,如果一个PasswordResetRequested
事件产生了,但系统在PasswordResetProcessManager
可以创建短信之前就崩溃了,那么短信最终会被发送,但只有在PasswordResetRequested
事件被重新播放时(这可能是未来很长一段时间)。例如,最终一致性的“最终”部分可能需要很长时间。
上述方法有效(我可以看到它也应该在更复杂的场景中工作,就像OrderProcessManager
这里描述的:https ://msdn.microsoft.com/en-us/library/jj591569.aspx )。但是,我很想听听其他人对这种方法的看法。