使用Node.js和DDD、CQRS以及Event Sourcing构建微服务(二)


概览

构建微服务要求整体思维。理解微服务架构及其适用规则很重要。这是什么意思呢?思考的一种方式是理解给定应用程序里的所有域,以及服务边界的细分。

微服务必须完全和表示给定域的关联上下文匹配——应该是松耦合高内聚的。
ovenview.png

第一部分,我们讨论了示例项目中使用的DDD,CQRS以及event sourcing模式。希望你已经查看了GitHub代码库并且复制了代码。这一部分会详细讨论实现细节——我也推荐大家超越本文的范畴,学习更多的相关概念。

这里将项目的微服务架构分解为流程图:
2.png

  • 域模型实现了聚合根,是微服务的核心——它通过command处理器聚合的角度验证并且持久化数据。
  • event在处理给定事件之前自动持久化到event store。


使用DDD带来的额外好处是,可以创建易于理解的目录树。这是示例项目的目录树:
3.png

注意:通过拷贝给定.env.example文件在根目录创建名为.env的新文件。

选择Node.js技术是因为它遵循模块化的方案构建应用程序——它完美契合微服务以及Unix的设计哲学:


小而美。让每个程序只干一件事情。(来源
模块化仅仅是构建长期运行的应用程序的一个方面。选择最佳实践以及设计模式也很重要——从开发人员和编译器的角度看,这些方面需要仔细思考。

选择Nest.js作为框架是因为Nest.js框架库遵循最佳实践,并且提倡最佳的设计模式,比如依赖注入。这些模式改进代码质量和可维护性——并允许进一步改进。阅读Nest的文档,在继续之前了解它的基础特性。

接下来让我们看看代码!

这是AppModule,引入了UsersModule
4.png

注意:AppModule用来注册微服务使用的所有模块。在示例项目中,涉及的模块包括UsersModuleEventStoreModule

很简单对吧?这是UsersModule:
5.png

注意:UsersModule用来bootstrap所有Users域特定的逻辑。

User请求

6.png

在写这篇文章的时候,Fastify处理大约76835请求/秒,Express处理大约50933请求/秒。我的项目实现了Fastify,因为我们希望得到尽可能大的吞吐量。可以很容易得从Fastify改成其他的路由框架。

文档是UsersModule里控制器实现的核心!这很有用,通过流化创建及维护微服务API文档的流程可以节省大量时间。
7.png

本项目最重要的是创建用户的流程:
8.png

通过用户接口触发command。当UserCreatedEvent被捕获后,userCreated saga继续欢迎用户的事务。

Commands——实现&处理器

9.png

聚合的所有变化都必须通过command来处理。当某个command被调度后,相关的command处理器验证并恢复给定请求——如果成功了,会尝试将新事件持久化到event store里。


客户端应用程序创建command,随后发送到域层。Command是通知某个特定实体执行特定操作的消息。(来源
10.png

CreateUserCommand实现:src/users/commands/impl/create-user.command.ts

注意:command实现用来构建command。实现应该易于理解——即使对非开发人员来说。
11-1.png

CreateUserCommand处理器 :src/users/commands/handlers/create-user.command.ts

注意:command处理器用来管理某个命令请求。在调度事件时需要按顺序commit。

聚合根——验证业务的域模型

11.png

域模型是域规则定义的地方。用聚合,而不是域模型来描述User——在DDD里,聚合是域对象的集合,可以当作单个单元来处理。记住,域模型对象是不可变的,因此需要聚合。


聚合形成对象关系的树形结构或图。聚合根是“最上层“的那个对象,它代表整体,也可能委托给其他对象。它很重要因为外部是通过它来通信的。(来源
12.png

User:src/users/models/user.model.ts

注意:User模型仅仅触发相关的事件。

13.png

UserRepository ——src/users/repository/user.repository.ts

注意:Users repository使用User模型来持久化并且处理事件。

存储事件——乐观锁&幂等

14.png

event sourcing领域的并发是很重要的。使用分布式command处理器时,聚合的并发访问或者修改一般不是个问题——可以做一定程度的冲突管理。

示例项目选择了乐观锁——了解event sourcing里乐观vs.悲观锁机制也很重要。

Event Store使用如下端口:
http://localhost:1113 (tcp)
http://localhost:2113 (http)

Event Store的所有HTTP请求都是幂等的。

发布事件到event store很简单,代码如下:
15.png

Event Bu发布:src/event-store/event-store.ts

在项目的event-store.ts文件里,包含Event Store bridge的逻辑,它订阅了给定域分类流里的所有事件。在这里分类流是Users域——它使用了名为$ce-users的类projection。

Event Store自带接口。当事件发生时,接口会通知用户,并且实时处理事件。
16.png

用Docker运行在本地的Event Store实例:http://localhost:2113

event store的设计保证event持久化和流化的原子性。可以试试,发送请求创建新的User,然后监控Event Store看看发生了什么。

Event由处理器实现——和command类似。
17.png

UserCreatedEvent实现:src/users/events/impl/user-created.event.ts

注意:事件实现用来构造事件。
18.png

UserCreatedEvent处理器 :src/users/events/handlers/user-created.handler.ts

注意:事件处理器用来管理事件。

事件处理器负责写入物化状态或者缓存来更快的读取。

最后,User saga继续Users创建事务。
19.png

UsersSagas:src/users/sagas/users.saga.ts

UserCreatedEvent事件存储之后,系统发出由userCreated saga触发的UserWelcomedEvent——之后用户创建事务完成。

读取物化数据

20.png

user command小结

21.png

  • command是一个对象,由用户通过用户接口发送
  • 域模型定义了将变更反应到域聚合的规则
  • command处理器在调度事件之前验证。


user查询小结

22.png

  • 数据库里的物化数据是由event数据的非规范化创建的。
  • 使用projection可以回放事件,从而重新创建数据


运行项目

$ ./scripts/up.sh            ## to boot up
$ ./scripts/down.sh          ## to shut down

结论

事件让我们可以关注于域而不是数据库schema,跨团队的合作也更为容易。

我们介绍了如何构建一个微服务,解释了DDD,CQRS和event sourcing的概念。克隆GitHub的代码库,自己多做尝试。

记住所有的模式都有自身的优缺点,CQRS和event sourcing模式也不例外——不是解决一切问题的银弹。

真实世界里

  • 外部应用程序也可以和微服务事件交互


一些涉及CQRS/ES的案例

  • 审计系统
  • 股票交易系统
  • 许可证平台
  • 数据库系统


我一直保持学习和实验,本文的示例项目还没有完结。借着分享构建项目的经验,来感谢所有参与贡献OSS(开源软件)技术的人。

下面是项目连接和联系方式。


原文链接:Building Microservices: Using Node.js with DDD, CQRS, and Event Sourcing — Part 2 of 2(翻译:崔婧雯)
===========================
译者介绍
崔婧雯,现就职于IBM,高级软件工程师,负责IBM WebSphere业务流程管理软件的系统测试工作。曾就职于VMware从事桌面虚拟化产品的质量保证工作。对虚拟化,中间件技术,业务流程管理有浓厚的兴趣。

0 个评论

要回复文章请先登录注册