在最近的一个涉及基于事件的CQRS系统的项目中,我们决定做一些与大多数谈论的解决方案相比似乎有些不同的事情。 但是,它们使我们获得了一些不错的属性,否则这些属性将很难(如果可能的话)。
事件存储为常规表
我们决定将事件存储作为RDBMS中的常规表来实现。 我们使用了PostgreSQL,但是这里几乎没有特定于PostgreSQL的内容。 我们知道该数据库非常可靠,功能强大且非常成熟。 最重要的是,单节点ACID事务提供了一些非常好的好处。
该表以以下字段结尾:
-
event_id
(int)–来自全局序列的主键 -
stream_id
(UUID)–事件流的ID,通常是DDD聚合 -
seq_no
(int)–特定流历史记录中的序列号 -
transaction_time
(时间戳)–事务开始时间,对于一次事务中提交的所有事件都相同 -
correlation_id
(UUID) -
payload
(JSON)
并非所有这些对于事件存储都是必需的,但是有一个重要event_id
常见的区别: event_id
–全局,顺序增加。 稍后我们将解决这个问题。
你可以做到吗?
如果您在常规数据库表中进行事件存储,则获得这样的全局事件ID极其便宜。 数据库确实非常有效地生成,存储,索引等列。 唯一的实际问题是,您首先是否可以负担得起使用数据库表。
我们一直在构建的系统并不面向广泛的网络。 它旨在供拥有数百或数千用户的公司内部使用。 这是一个相对较低的规模,而Postgres不会有任何问题。
总而言之,如果您要构建下一个亚马逊,那不是我不推荐的东西。 但是您并非偶然,因此您可以负担得起使用简单技术的奢侈。
全局顺序事件ID的好处
现在我们有了这个特殊的事件ID,我们该怎么办?
让我们看一下事件存储的读取接口:
public interface EventStoreReader {
List<Event> getEventsForStream(UUID streamId, long afterSequence, int limit);
List<Event> getEventsForAllStreams(long afterEventId, int limit);
Optional<Long> getLastEventId();
}
第一种方法很明显,随处可见。 我们仅使用它从事件存储中还原单个流(聚合)以处理新命令。
其他两个使用事件ID,在特定事件之后返回一批事件,以及最后一个事件的ID。 它们是我们的读取模型(投影)的基础。
读取模型是通过轮询(带有提示)事件存储来实现的。 他们记住最后处理的事件的ID 。 每隔一段时间(或当事件存储中的通知唤醒时),他们会从存储中读取下一批事件,并在单个线程中按顺序处理它们。
这种线性单线程处理可能会尽可能简单,但显然可伸缩性有限。 如果每分钟收到600个事件,则意味着无论如何,平均每个事件您的速度都不会慢于100 ms。 实际上,您还需要考虑开销并留出一些空间,因此它需要比这更快。
可以通过在读取模型中分片或并行化写入来解决该问题,但目前我们还没有发现这一点。 并行运行多个独立的专业模型无疑可以帮助实现这一目标。
将投影的最后处理的事件ID与当前的全局最大值进行比较,您可以立即知道该投影背后有多少 。 这在逻辑上等同于队列大小。
全局序列也可以用来减轻最终一致性 (或陈旧性) 的不利影响 。
执行命令可能会返回上一个写入事件的ID。 然后查询可以使用该ID,并要求:“我可以等待5秒钟,但是如果您的数据早于该ID,请不要给我结果”。 在大多数情况下,这仅是几毫秒。 对于该价格,当用户进行更改时,她会立即看到结果。 这是来自服务器的实际数据,而不是通过在用户界面中复制域逻辑来实现的模拟!
在域方面也很有用。 我们有一些应用程序和域服务,它们查询某些特定于域的预测(例如,进行唯一检查)。 如果您知道事件存储中的最后一个事件是X,则可以等到投影赶上该点,然后再对该命令进行进一步处理。 这就是解决通常用传奇解决的许多问题所需要的全部。
最后但并非最不重要的一点是,由于所有事件都是有序的, 因此投影始终是一致的 。 它可能会落后几秒钟或几天,但绝不会前后矛盾。 根本不可能遇到这样的问题,例如一个流要处理到星期一,而另一个流要处理到星期四。 如果在发生特定事件之前发生了某些事情,则视图模型中始终保持相同的顺序。
它使代码和系统状态更易于编写,维护和推理。
再谈可扩展性和复杂性
无论实际的客户需求和实际规模如何,都有使用复杂,可扩展性高的技术的趋势。 这样的工具虽然占有一席之地,但并不是显而易见的赢家,没有解决所有问题的金锤子。 此外,如果考虑到开发和运营的复杂性及其局限性,那么它们确实非常昂贵。
有时,使用更简单的工具可以很好地解决问题。 您不仅可以节省开发和运营成本,还可以使用一些真正强大的工具,而这在大规模生产中是不可能的。 包括全局计数器,线性化和ACID事务。
我们的示例显示了一个系统,该系统足够复杂,可以使用CQRS保证事件来源,但是规模足够小,即使在线性Postgres数据库中,也可以使用线性事件存储(甚至具有线性投影)。
选择无聊技术有很多充分的原因。 如果要进行创新(应该这样做),请谨慎选择为什么要这样做,并且不要同时在所有领域进行创新。
翻译自: https://www.javacodegeeks.com/2015/09/achieving-consistency-in-cqrs-with-linear-event-store.html