由于您未指定“写入”数据库的用途,因此在此我要假设的意思是:将新更新注册到聚合中时,您无需从事件存储中重建聚合,而是从“写入”数据库中删除它,验证更改并发出事件。
如果这是您的意思,那么该策略将为不一致创造条件:如果在最后一个机会将其放入“写入”数据库之前进行了新的更新,则该新更新将针对过时的数据进行验证,因此有可能发出“不可能”(即“不允许”)事件并破坏系统状态。
例如,考虑在剧院预订座位的典型例子。为防止重复预订,您需要确保预订的座位尚未被占用-这就是您所说的“验证”。为此,您将已预订的座位列表存储在“写”数据库中。然后,当收到预订请求时,您检查所请求的座位是否在列表中,如果不是,则发出“已预订”事件,否则返回错误消息。然后,您运行一个投影过程,在此侦听“已预订”事件并将已预订座位添加到“写入”数据库的列表中。
通常,系统的功能如下:
1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Projection process catches the event, adds seat #1 to the "already booked" list.
5. Another request to book seat #1.
6. Check in the list: the list contains seat #1
7. Respond with an error message.
但是,如果请求传入的速度太快,而步骤5在步骤4之前发生,该怎么办?
1. Request to book seat #1
2. Check in the "already booked" list: the list is empty.
3. Issue a "booked seat #1" event.
4. Another request to book seat #1.
5. Check in the list: the list is still empty.
6. Issue another "booked seat #1" event.
现在,您有两个事件可预订同一座位。系统状态已损坏。
为防止这种情况发生,您永远不要针对投影验证更新。要验证更新,请从事件存储区重建聚合,然后根据事件验证更新。之后,您发出一个事件,但是使用时间戳保护以确保自从您上次从商店读取以来没有发出新的事件。如果失败,则重试。
从事件存储中重建聚合可能会降低性能。为了减轻这种情况,您可以将聚合快照直接存储在事件流中,并用创建快照的事件ID进行标记。这样,您可以通过加载最新快照并仅重播快照之后的事件来重建聚合,而不是始终从一开始就重播整个事件流。