CQRS解构: 用读写分离设计API

18-09-18 banq
                   

本文讨论的是如何使用CQRS实现API设计。

概述
下面是名为Command / Query Responsibility Segregation(CQRS)的设计模式:

返回数据 做出改变
查询 ✔️ ❌
命令 ❌ ✔️

查询和命令是两种分离的API。

为何使用这种模式?我喜欢它有几个原因。作为API的消费者,我永远不必担心使用API出现异常了;相反,我能确切地知道哪些API调用会专门应付对系统的更改请求,没有含糊之处。这使得API 易于推理。

我曾经尝试创建一个统一的界面来完成两者,但是随着时间推移,出现“服务于两个主人“”的典型问题,单一界面变得更加混乱。

时间长了会发生:“我们不使用这个字段,为什么我们要更新它?。” 回应:“我不知道,但继续这样做或会出问题。”
📦🛐

因此,CQRS的核心是一个关注点分离的特定应用,即良好的组织实践。现在我们已经对模式进行了介绍,我将介绍一些实现细节和经验教训。

消息
我认为每个查询或命令都是一条消息。这意味着任何客户端系统都可以将这些表达为没有方法的普通数据(类或结构)。然后以线形格式(如JSON或CapnProto或w / e)轻松传输它们。

每条消息都有一个名称 - 通常只是类/结构名称 - 它在API中唯一标识它。如SearchCustomers(查询)或DeactivateCourse(命令)。名称用于标识请求的操作,然后将其与消息解析器和处理函数进行匹配。安全授权由此很简单,只要保留一个用户名单,允许这些用户可发送哪些消息名称;然后在处理任何用户的消息之前检查该列表。🤘🤘

运营
命令和查询应该如何工作似乎很明显。但是我发现了一些细微差别。

查询:
查询是很简单的:

1. API侦听 /query/[Query Name]
2. 验证用户是否具有[Query Name]权限
3. 反序列化查询消息
4. 将查询消息传递给其处理函数,该函数将:
5. 验证查询消息
6. 从数据库加载和转换数据
7. 序列化并返回数据

命令:
命令的目的是在系统上执行一些业务操作。在实践中,我们注意到命令是否需要更改一个或多个实体。出于架构原因,如何处理多个实体更改非常重要。

📌 在这种情况下,实体意味着某个逻辑单元。在高度规范化的表中,实体可能包含父对象和一对多关系的任何子对象。在DDD术语中,你可以将其称为聚合。在事件溯源中,这是一个事件流。

可扩展性
你可以在单个事务中执行多实体更改,以实现全有或全无的事务语义。这种方法很适合,但它限制了可伸缩性。要参与事务,所有受影响的实体必须位于同一数据库节点上。如果它们位于不同的节点上,则发生分布式事务(如果数据库支持)。随着负载的增加,分布式事务将逐渐变慢。跨实体事务是企业内部业务应用程序的有效方法,但对于公开的互联网服务,也许不是。

像互联网这种更加大规模级别应用,我们的方法是仅使用单实体命令进行更改。当用例需要更改多个实体时,请使用元命令(自身不做任何改变),而不是编排并运行单实体命令。我将单实体命令称为“基本命令”,将多实体命令称为“工作流程”。当工作流调用基本命令时,这些命令可能会失败,工作流程会以业务用例的方式处理故障。这可能意味着需要忽略失败并作为业务错误/警告返回给客户,或采取补偿回退措施。

客户端工作流
你可以在客户端实现工作流 - 让UI编排所有必要的基本命令。但是,我不在客户端实现工作流,而在API端实现工作流,主要理由是清晰(特别是安全性)。

我用一个真实例子来说明。我们有培训师的角色,这个角色不能创建课程,但是,他们可以记录他们提供给员工的培训。记录培训时可能需要创建一个有限选项的新课程。将记录培训作为API工作流执行,可以将其表示为单个细化权限:“培训师可以记录培训,但不能创建课程。” 在权限UI上选中一个选项,而不选中另一个选项。

如果采取客户端工作流,执行上述相同的操作,我们看看会怎样?我们需要添加一个基本命令:创建培训师课程,然后管理员用户必须被告知:“要给某人录制培训的权限,你必须检查Create Trainer Course并且Permission X和Permission Y。” 那么这就给最终用户造成的负担,需要他做这些流程。在这里,我们还可以创建一个伪命令,仅用于权限目的,映射到所需的基本命令。这就将负担又转移给开发人员。我不喜欢这些结果中的任何一个,所以我更喜欢API端工作流。

指导原则
在实施CQRS API时,人们会提出一些非常常见的问题。

1. 返回错误与返回数据不同。
一个流行的误解是命令应该什么都不返回。实际是命令应该返回一些东西。它们返回有关操作本身的元信息(无论是成功还是失败以及为什么。这与返回业务数据非常不同,后者是查询的工作。

2. 命令可以在不进行更改的情况下成功。
命令可以进行0次或更多次更改。换句话说,“进行更改”是命令的目的,而不是所需的结果。因此,对于成功运行的命令完全有效,但不会导致任何更改。

在运行命令之前和之后比较实体,如果它们完全相同,那么就是进行0更改并成功返回。

3. 命令处理代码可以调用查询。
这似乎违反了CQRS原则。命令的内部除了“进行更改”之外,它没有任何意思。

因此,可以在命令中运行查询,才能获取命令所需的一些信息,但是要小心一点。

4. 自动递增ID不应是主键ID。
常常需要返回自动生成的ID,因为自动增量ID非常方便,但是也有问题:重复。

场景:用户填写表单用来创建新实体并点击提交。请求超时。

自动增量冒险:如果自动增量字段是您唯一的ID,则你的应用无法知道请求是否成功。对这种情况的补救措施通常取决于用户的意识和参与。

如果用户再次点击提交(非常可能),但是先前的请求如果确实创建了实体,尽管超时,那么现在有两个具有不同ID的相同实体。要正确清理,用户现在应该搜索重复项并删除冗余实体​​(极不可能)。

或者,在超时之后,用户可以搜索他们可能创建的实体。如果他们找不到,请再回来填写表单。根据我的经验,这种情况不太可能。如果培训用户习惯于这样思考,则可能会发生这种情况。

还可以添加重复检查的外部系统,例如保持对已查看操作及其结果的记忆。但有更好的方法......

预先生成的ID(冒险):
在用户甚至开始键入任何内容之前,在加载表单时就生成(或从服务器请求)ID。

在用户被告知请求超时后,她再次点击提交。界面就会使用相同的预生成ID发送与之前相同的完全相同的请求。如果数据库没有重复会成功,否则API响应:“此实体已存在。” 如果UI可以识别出这个特定错误,它可以假装它正常成功。这种冒险带来了更好的用户体验,没有重复的机会。

我们的策略:我们倾向于使用UUID进行所有标识。它们很容易在许多平台上生成。他们无视趋势分析。我们的大多数创建表单都必须运行查询(例如,获取下拉列表数据),因此我们会在查询结果中包含一个新的UUID。

结论
命令是变动的守门人。查询则是知识库,这是CQRS。我发现这种模式使我走向正确的方向。它也是一种多功能的模式。它不关心你的部署的是单体还是微服务。甚至可以将命令和查询拆分为各自独立的服务,实现读写负载分离。

但请记住,这只是一个大系统中的一个部分,而不是适合每个系统的通用工具。CQRS模式在后端系统的边界内能很好地工作,与客户端应用程序连接。与任何模式一样,只有在适当的情况下应用它时才有用。

A Deconstruction of CQRS - DEV Community 👩‍💻👨‍?

                   

3