选择一个合适的架构
从头开始创建一个银行与你通常周日散步的方式完全不同。想象一下,这更像是在一个遥远而未知的丛林中徒步旅行。对于所有困难和长时间的活动,它需要良好的准备和精心挑选工具,以便快速移动,不会半途而废。
在软件中,起决定性的工具不是我们使用的数据库或框架,它甚至不是编程语言,而是架构。简而言之,这就是我们该如何组织代码,使其满足业务领域的技术和运营要求。
我们为核心银行系统定义了至少五个关键要求。
- Traceability(可追溯性)在审计的情况下,我们必须能够知道该数据是由哪些操作导致的.
- Performance(性能) 我们的客户看到的银行帐户和业务,但我们的会计师看到的交易在一个非常不同的方式,但两者都应该受益于一个非常快的界面;
- Availability(可用性) 我们有责任保持和提高人们对支付系统的信心,因为我们不能及时回复授权请求而拒绝银行卡支付将是不可接受的;
- Consistency(一致性) 每次的交易应该是一个原子性的;
- Maintainability(可维护性)我们必须能够快速诊断问题并修复它们。你不会乱花别人的钱。此外,我们的代码应该得到全面的测试,整个团队应该能够在不踩到彼此脚趾的情况下工作(在各自的领域下工作)。
可追溯性
我们的业务主要是在于我们的数据。钱本身就只是数据,账户余额是数据库中某个地方的一个数字,它可以让你支付房租、账单和食物。但这还不够,我们希望我们的数据完全透明,能够证明账户上发生的每一项操作都是合理的,从而导致账户当前的余额。我们的架构必须允许通过设计实现这一点。
软件架构中出现了一种名为Event Sourcing(ES)的新模式。这个词最早出现在2005年的Martin Fowler的一篇文章中,但在Greg Young的推动下,2011年开始引起公众兴趣。他2014年的会议讲座是关于这个主题的参考。 ES的口号是系统的状态由导致该状态的所有事件给出。这些事件一旦发出就永远不会被修改,并存储在只会追加的事件存储库(append-only storage 注:就是这个存储库里的东西不会被删除,修改,只能是新增,删除其实就是新增了一个删除事件)中。

这种方式非常适合可追溯性,系统中发生的所有事情都在我们的数据库中。但现在即使最简单的查询也变得非常复杂。例如,获得帐户余额会迫使我们在事件存储中把相关的事件进行迭代,比如存款和取款。虽然这看起很简单,它会随着事件的堆积越来越多从而导致查询变得越来越慢。
性能
这就是为什么ES与另一种称为Command Query Responsibility Segregation(CQRS)模式结合使用的原因。此模式使用两个非常不同的实体处理系统中的读取和写入。在这种配置中,ES提供了它们之间非常自然的边界。该架构通常称为CQRS / ES。

查询端读取来自事件存储的事件,并具有自己的数据库。在收到每个事件之后,它相应地更新自己的数据库。

BankAccount称为事件流的(projection )视图。这种方法在性能和可维护性方面具有极大的优势。
- 我们可以有任意多的视图。例如,一个用于包含用户、帐户和操作的客户机API。另一种为会计API,包含总分类账、会计变动、资产和负债。这些视图是完全解耦的,因为真相的唯一来源是事件。
- 我们可以很容易地添加或修复现有的视图。我们所需要做的就是使用新的视图重放所有事件,然后切换到新的数据库。
- 我们不必在所有的地方都使用SQL,我们可以为每个视图选择不同的存储,具体取决于我们将要进行的查询类型。例如,如果我们需要进行与图形相关的查询,我们可以在Neo4J数据库中使用这些事件。我们还可以将事件传送到ES中去,以进行非常快速的搜索。
这些视图的另一个优点是它们可以独立工作,这使我满足了我们的另一个要求。
可用性
与大多数系统一样,我们的系统需要处理比写入多得多的读操作。然而,我们需要在每个请求上保持较短的响应时间,尤其是在写入方面。如果我们对卡交易授权的响应时间太长,则交易将被拒绝,从而导致我们的客户感到失望。
这就是为什么事件存储必须是异步的原因。这样,写入方可以简单地向其发布事件并继续执行接下的操作,而不是等待所有视图去处理它们。这些事件将投到不同的机器,我们可以吸收大量的查询流量而对写入方面没有任何影响。如果读取方面变得不堪重负,我们甚至可以在多台机器上复制相同的视图,所有机器都连接到事件存储。

虽然这对性能非常有益,但现在有一个问题。
写一致性
我们基于给定的命令和系统的当前状态在写入方做出业务决策。如果这个状态在视图端,我们可能会进行脏读,从而根据过时的状态做出决定。
为了解决这个问题,我们将写入部分分成称为聚合的小型有状态组件。其中一个是“银行账户”。聚合处理命令并根据其内部状态生成事件。在以下示例中,这个state就是银行帐户余额。

聚合使用其状态来做出业务决策。例如,由于资金不足,它可以拒绝撤销命令。

由于每个银行帐户只有一个聚合实例,因此可确保一致性。您还会注意到我们不需要依赖事务和数据库锁。
好消息! Elixir的actor模型非常适合这些聚合。在Elixir中称为GenServer的actor具有状态并在其邮箱中接收消息。它通过设计保证两个消息不能同时处理。它的启动和停止也非常便宜,其中数十万个可以轻松地在一台机器上运行。
可维护性
我们的最终标准是可维护性。 CQRS / ES架构比传统架构更难设置,但它随着时间的推移提高了可维护性。
系统的大多数部分都是纯函数,这意味着它们非常简单易于测试。聚合(我们的大多数业务逻辑都在聚合)接收命令并生成没有副作用的事件。大多数测试都会注入一些命令并简单地检查生成的事件。
由于系统的大多数部分都很小并且彼此分离,因此多个开发人员可以轻松地在不同部分上工作而不会发生冲突。
视图对错误非常宽容,因为我们可以通追溯事件来修复错误。例如,如果我们在会计视图中进行了错误的舍入,我们不需要在修复后迁移数据,我们只需要重放事件。
虽然CQRS / ES并不完美,但我们仔细考虑了所有方面。
缺点
CQRS / ES是一种非常复杂的模式,在处理有些业务上可能比传统系统更复杂。
单一性检查就是一个很好的例子。由于聚合不能相互通信,我们如何检查用户电子邮件是否已被接收?
很清楚地表明CQRS / ES很少适合整个系统,应该谨慎使用。例如,将它用于用户管理和角色没有多大意义,并且这个更适合使用传统的CRUD。
我们如何处理需要写入然后立马来的读请求?例如,注册用户的POST请求应该返回创建的用户,但是立马来的查询命令通过查询视图查不到数据,因为视图是异步更新的。
这些案例很少在实践中发生,如果他们这样做了,则有一些解决方法。
如果事件在事件存储中是不可变的并且必须可重放,那么我们如何处理事件中的重大变化呢?
简短的回答,我们不会做出重大改变。迁移先前的事件是危险的,有点像时间旅行和无可挽回地改变事件的过程。相反,我们为每个事件添加一个版本,并在聚合和投影中处理不同的版本。这意味着我们在设计这些事件时必须非常小心。但是,必须注意的是,例如,处理标准HTTP API的版本控制并不困难。
我们如何处理副作用(注:就是可能会产生新的流程)?例如,我们如何安排未来的作业、向用户发送确认电子邮件或与外部系统交互?
DDD / CQRS还有一个额外的概念叫做“Sage”或“流程管理器”。它会对域事件做出反应并产生新的流程。

总结:
希望您现在对CQRS / ES及其如何满足我们IT基础的需求有一个很好的大体印象。以额外的初始复杂性为代价,我们相信它将帮助我们在正确的方向上扩展并实现我们的目标。基于纯粹的功能,特别是非常适合actor,这种选择与我们选择使用Elixir作为语言和平台是一致的。
如果你碰巧在巴黎(或者很乐意搬迁),有一种疯狂和不可抗拒的冲动,需要学习,编写和发扬CQRS / ES架构中的代码:让我们来谈谈吧!
原文:https://medium.com/margobank/choosing-an-architecture-85750e1e5a03