微服务领域设计相关笔记
程序员文章站
2022-06-24 17:57:32
...
`最近一个项目用的技术比较新,整理一些相关的知识,主要是领域设计与saga分布式事务处理,还有引申到cqrs与基于api优先开发。网上的知识比较杂,所以所这些相关的汇总一下,红色粗体是自己的思考。`
是无法嵌
## `1. 微服务与DDD`
https://www.cnblogs.com/davenkin/p/6612656.html
- `两者默契`
`在[微服务](https://martinfowler.com/articles/microservices.html)(Microservices)架构实践中,人们大量地借用了DDD中的概念和技术,比如一个微服务应该对应DDD中的一个限界上下文(Bounded Context);在微服务设计中应该首先识别出DDD中的聚合根(Aggregate Root);还有在微服务之间集成时采用DDD中的防腐层(Anti-Corruption Layer, ACL);我们甚至可以说DDD和微服务有着天生的默契。`
- `领域事件`
`在DDD中有一条原则:一个业务用例对应一个事务,一个事务对应一个聚合根,也即在一次事务中,只能对一个聚合根进行操作。实际应用中,我们经常发现一个用例需要修改多个聚合根的情况,故引入领域事件。领域事件作为已经发生过的历史数据,在建模时应该将其创建为不可变的特殊值对象。存在多种方式用于发布领域事件,其中“在聚合中临时保存领域事件”的方式是值得推崇的。为了避免事件重复带来的问题,最好的方式是将事件的消费方创建为幂等的。`
1. `解耦微服务(限界上下文)`
2. `帮助我们深入理解领域模型`
3. `提供审计和报告的数据来源`
4. `迈向[事件]等`
- `事务性`
`如果JTA不是你的选项,那么可以考虑采用事件表的方式。这种方式首先将事件保存到聚合根所在的数据库中,由于事件表和聚合根表同属一个数据库,整个过程只需要一个本地事务就能完成。然后,在一个单独的后台任务中读取事件表中未发布的事件,再将事件发布到消息中间件中。`
- `持久化`
`其实对于DDD中的聚合根来说,NoSQL是相比于关系型数据库更合适的选择,比如用MongoDB的Document保存聚合根便是种很自然的方式。但是多数NoSQL是不支持ACID的,也就是说不能保证聚合更新和事件发布之间的原子性。还好,关系型数据库也在向NoSQL方向发展,比如新版本的PostgreSQL([版本9.4](https://www.postgresql.org/docs/9.4/static/datatype-json.html))和MySQL([版本5.7](https://dev.mysql.com/doc/refman/5.7/en/json.html))已经能够提供具备NoSQL特征的JSON存储和基于JSON的查询。此时,我们可以考虑将聚合根序列化成JSON格式的数据进行保存,从而避免了使用重量级的ORM工具,又可以在多个数据之间保证ACID,何乐而不为?`
- `值对象`
`聚合根:独立存在的对象,是代表某个限界上下文中的一个高内聚的整体概念。他的生命周期是其所属上下文中所承担的职责的周期。`
`实体:无法独立存在,是聚合根的一部分,生命周期由所属的聚合根掌控。`
`值对象:可以独立存在,但是无法进行自我管理,可以描述任何聚合根/实体,无生命周期的概念,也可以理解为永生(无限生命周期)。`
`聚合根与实体比较好理解,比如订单与订单中的项目。值对象比如就是聚合根的相关的属性的集合,比如地址对象,一般是拆开细节属性,与所属聚合根或者实体的属性一起存贮。`
`**不过我在考虑用hash当ID,另存一个表。hash来自值对象的每一个值,值对象有hash方法,值对象属性都相同则hash相同,只存一条记录。**`
`另外,如果采用ddd方式,由于专注于对邻域对象的操作,命令,以及事件处理,这些特点必然从系统层面进行分离,集中处理邻域对象的变化,于是有了CQRS。以往只是数据库层面的读写分离,发展到了系统代码层面的分离了。`
## `2. 分布式事务一致性`
`https://www.cnblogs.com/skyesx/p/9697817.html`
`https://cloud.tencent.com/developer/article/1545301`
`这个理论就不说了,ACID,BASE,CAP..,重点提一个saga模式。`
`从简单到复杂,可以选择多种方式:`
> `基于消息实现的事务:适用于分布式事务的提交或回滚只取决于事务发起方的业务需求,其他数据源的数据变更跟随发起方进行的业务场景。本地提交事务,发消息即可。`
> `但是基于消息实现的事务并不能解决所有的业务场景,例如以下场景:某笔订单完成时,同时扣掉用户的现金。`
>
> `这里事务发起方是管理订单库的服务,但对整个事务是否提交并不能只由订单服务决定,因为还要确保用户有足够的钱,才能完成这笔交易,而这个信息在管理现金的服务里。这里我们可以引入基于补偿实现的事务,本地事务不提交,远程调用,根据结果提交或者回滚。若不能使用基于消息队列的最终一致性事务,那么可以优先考虑使用基于补偿的事务形态。`
>
> `阿里GTS/fescar本质上也是这补偿的编程模型,只不过补偿代码自动生成,无需业务干预,同时接管应用数据源,禁止业务修改处于全局事务状态中的记录。因此,其关于读场景的适用性,可参考补偿。但其在写的适用场景由于引入了全局事务时的写锁,其写适用性介于 TCC以及补偿之间 。`
### `TCC实现的事务`
`每个业务实现try,confirm,cancle,本地事务都提交,但业务上是保留资源,比如冻结金融并不真正扣除。`
`比基于补偿实现的事务的流程要复杂,同时开发的工作量也更多。TCC实际上是最为复杂的一种情况,其能处理所有的业务场景,但无论出于性能上的考虑,还是开发复杂度上的考虑,都应该尽量避免该类事务。`
`很多金融核心以上的业务(比如在渠道层、产品层、集成层的系统),这些系统的特点是最终一致即可、流程多、流程长、还可能要调用其它公司的服务(如金融网络)。这是如果每个服务都开发 Try、Confirm、Cancel 三个方法成本高。如果事务中有其它公司的服务,也无法要求其它公司的服务也遵循 TCC 这种开发模式。同时流程长,事务边界太长会影响性能。`
### `Saga(*Eventuate* *Tram* *Saga*)`
`核心以上的业务系统可以采用补偿事务,补偿事务处理方面在30年前就提出了 Saga 理论,随着微服务的发展,近些年才逐步受到大家的关注。目前业界比较也公认 Saga 是作为长事务的解决方案。`
`Saga有两个方式:Orchestration(有中心协调者)和Choreography(无中心协调者)。`
`最近一个项目中是用Eventuate Tram saga 的 Orchestration处理事务,也用Eventuate Tram框架处理领域事件。可以对比一下guava的eventBus。`
`https://developer.aliyun.com/article/725370`
`服务需要使用库来发送和接收消息。一种方法是使用消息代理的客户端库,但是直接使用这样的库有几个问题:`
`更好的方法是使用更高级别的库或框架来隐藏底层的细节,并直接支持更高级别的交互方式。为简单起见,本书中的示例使用了我的Eventuate Tram框架。它有一个简单易用的API,可以隐藏使用消息代理的复杂性。除了用于发送和接收消息的API之外,Eventuate Tram还支持更高级别的交互方式,例如异步请求/响应和领域事件发布。`
`使用开源Eventuate框架,这个框架是为事务性消息、事件溯源和Saga量身定做的。之所以选择,是因为它与依赖注入和Spring框架不同,对于微服务架构所需的许多功能,目前开发者社区还没有广泛采用的框架。如果没有Eventuate Tram框架,许多演示代码必须直接使用底层消息传递API,这会使它们变得更加复杂并且会模糊重要的概念。或者使用一个没有被广泛采用的框架,这也会引起批评。`
`相反,这些演示代码使用Eventuate Tram框架,该框架具有隐藏实现细节的简单易懂的API。你可以在应用程序中使用这些框架。或者,你可以研究Eventuate Tram框架并自己重新实现这些概念。`
`Eventuate Tram还实现了两个重要机制:`
`Eventuate Tram框架为Java应用程序实现事务性消息。它提供了一个相对底层的API,用于以事务方式发送和接收消息。它还提供了更高级别的API,用于发布和使用领域事件以及发送和处理命令式消息。`
### `方法选择`
```
```
`因SAGA事务的形态需要配合较为明显的前端业务交互变更,个人建议在单一事务执行过程较长、存在较多子事务,并且无法使用基于消息的事务形态时使用。`
## `3. 开发角度`
`分为:存储层(respository),领域对象层(domain),服务层,接口web层。`
#### `领域对象层`
`https://www.jianshu.com/p/4551c0248289`
`我们的核心实体(Entity)和值对象(Value Object)应该在Domain层,不要把Entity的属性和行为分离到Domain和Service两层中去实现,即所谓的贫血模型,事实证明这样的实现方式会造成很大的维护问题。作为元模型中元素之一的实体本身就应该包含针对自身的行为定义。`
`一般返回对实体操作后,返回ResultWithDomainEvents,即包含结果,又包含事件。比如修改后,返回包含【实体】与【包含dto对象的事件】的组合结果。`
#### `服务层`
`通常持有持久化对象Repository,与事件发布对象EventPublisher。比如处理实体修改的操作,包括:`
- `根据id从存贮层得到实体`
- `对实体执行实体自身的操作`
- `对操作后返回的ResultWithDomainEvents进行处理,包括保存修改后的实体与发布事件操作。`
`服务层要么不返回结果,比如修改不返回,只是void。或者返回dto对象。`
`此外,还包含event处理包(领域事件处理用),command处理包(saga命令处理用)。`
#### `API`
`https://www.jdon.com/53930`
`遵循API优先方法,我们在开始编码之前先指定一个API。通过API描述语言,团队可以进行协作而无需执行任何操作。`
`API层包含领域事件对象,以及request对象,返回的vo对象。`
`使用openApi标准的yml接口规范文档,使用swagger-codegen-maven-plugin的maven插件,同时引用上面的对象,可以生成java API的代码框架。`
是无法嵌
## `1. 微服务与DDD`
https://www.cnblogs.com/davenkin/p/6612656.html
- `两者默契`
`在[微服务](https://martinfowler.com/articles/microservices.html)(Microservices)架构实践中,人们大量地借用了DDD中的概念和技术,比如一个微服务应该对应DDD中的一个限界上下文(Bounded Context);在微服务设计中应该首先识别出DDD中的聚合根(Aggregate Root);还有在微服务之间集成时采用DDD中的防腐层(Anti-Corruption Layer, ACL);我们甚至可以说DDD和微服务有着天生的默契。`
- `领域事件`
`在DDD中有一条原则:一个业务用例对应一个事务,一个事务对应一个聚合根,也即在一次事务中,只能对一个聚合根进行操作。实际应用中,我们经常发现一个用例需要修改多个聚合根的情况,故引入领域事件。领域事件作为已经发生过的历史数据,在建模时应该将其创建为不可变的特殊值对象。存在多种方式用于发布领域事件,其中“在聚合中临时保存领域事件”的方式是值得推崇的。为了避免事件重复带来的问题,最好的方式是将事件的消费方创建为幂等的。`
1. `解耦微服务(限界上下文)`
2. `帮助我们深入理解领域模型`
3. `提供审计和报告的数据来源`
4. `迈向[事件]等`
- `事务性`
`如果JTA不是你的选项,那么可以考虑采用事件表的方式。这种方式首先将事件保存到聚合根所在的数据库中,由于事件表和聚合根表同属一个数据库,整个过程只需要一个本地事务就能完成。然后,在一个单独的后台任务中读取事件表中未发布的事件,再将事件发布到消息中间件中。`
- `持久化`
`其实对于DDD中的聚合根来说,NoSQL是相比于关系型数据库更合适的选择,比如用MongoDB的Document保存聚合根便是种很自然的方式。但是多数NoSQL是不支持ACID的,也就是说不能保证聚合更新和事件发布之间的原子性。还好,关系型数据库也在向NoSQL方向发展,比如新版本的PostgreSQL([版本9.4](https://www.postgresql.org/docs/9.4/static/datatype-json.html))和MySQL([版本5.7](https://dev.mysql.com/doc/refman/5.7/en/json.html))已经能够提供具备NoSQL特征的JSON存储和基于JSON的查询。此时,我们可以考虑将聚合根序列化成JSON格式的数据进行保存,从而避免了使用重量级的ORM工具,又可以在多个数据之间保证ACID,何乐而不为?`
- `值对象`
`聚合根:独立存在的对象,是代表某个限界上下文中的一个高内聚的整体概念。他的生命周期是其所属上下文中所承担的职责的周期。`
`实体:无法独立存在,是聚合根的一部分,生命周期由所属的聚合根掌控。`
`值对象:可以独立存在,但是无法进行自我管理,可以描述任何聚合根/实体,无生命周期的概念,也可以理解为永生(无限生命周期)。`
> `1.把值对象中的属性作为所属实体/聚合根的数据列来存储。` > > `缺点:会导致数据表列数较多,在一个数据页存储的数据量变少,影响数据库表的使用性能。` > > `2.把整个值对象序列化后作为所属实体/聚合根的数据列来存储。` > > `缺点:出现大数据长度的列,页会导致在一个数据页存储的数据量变少,影响数据库表的使用性能。另外无法直接通过SQL来查询值对象的属性,需要自定义做反序列化操作。` > > `3.如果是ORM类的框架,那么建立一个基类通过protected的标识列来委派这个唯一标识,但是在实际运用过程中是感觉不到这个存在的。` > `4.如果不是ORM框架或者本身框架支持,那么可以通过无主键的方式存入到数据表中。` > > `缺点:是无法嵌TAO模型` >
`聚合根与实体比较好理解,比如订单与订单中的项目。值对象比如就是聚合根的相关的属性的集合,比如地址对象,一般是拆开细节属性,与所属聚合根或者实体的属性一起存贮。`
`**不过我在考虑用hash当ID,另存一个表。hash来自值对象的每一个值,值对象有hash方法,值对象属性都相同则hash相同,只存一条记录。**`
`另外,如果采用ddd方式,由于专注于对邻域对象的操作,命令,以及事件处理,这些特点必然从系统层面进行分离,集中处理邻域对象的变化,于是有了CQRS。以往只是数据库层面的读写分离,发展到了系统代码层面的分离了。`
## `2. 分布式事务一致性`
`https://www.cnblogs.com/skyesx/p/9697817.html`
`https://cloud.tencent.com/developer/article/1545301`
`这个理论就不说了,ACID,BASE,CAP..,重点提一个saga模式。`
`从简单到复杂,可以选择多种方式:`
> `基于消息实现的事务:适用于分布式事务的提交或回滚只取决于事务发起方的业务需求,其他数据源的数据变更跟随发起方进行的业务场景。本地提交事务,发消息即可。`
> `但是基于消息实现的事务并不能解决所有的业务场景,例如以下场景:某笔订单完成时,同时扣掉用户的现金。`
>
> `这里事务发起方是管理订单库的服务,但对整个事务是否提交并不能只由订单服务决定,因为还要确保用户有足够的钱,才能完成这笔交易,而这个信息在管理现金的服务里。这里我们可以引入基于补偿实现的事务,本地事务不提交,远程调用,根据结果提交或者回滚。若不能使用基于消息队列的最终一致性事务,那么可以优先考虑使用基于补偿的事务形态。`
>
> `阿里GTS/fescar本质上也是这补偿的编程模型,只不过补偿代码自动生成,无需业务干预,同时接管应用数据源,禁止业务修改处于全局事务状态中的记录。因此,其关于读场景的适用性,可参考补偿。但其在写的适用场景由于引入了全局事务时的写锁,其写适用性介于 TCC以及补偿之间 。`
### `TCC实现的事务`
`每个业务实现try,confirm,cancle,本地事务都提交,但业务上是保留资源,比如冻结金融并不真正扣除。`
`比基于补偿实现的事务的流程要复杂,同时开发的工作量也更多。TCC实际上是最为复杂的一种情况,其能处理所有的业务场景,但无论出于性能上的考虑,还是开发复杂度上的考虑,都应该尽量避免该类事务。`
`很多金融核心以上的业务(比如在渠道层、产品层、集成层的系统),这些系统的特点是最终一致即可、流程多、流程长、还可能要调用其它公司的服务(如金融网络)。这是如果每个服务都开发 Try、Confirm、Cancel 三个方法成本高。如果事务中有其它公司的服务,也无法要求其它公司的服务也遵循 TCC 这种开发模式。同时流程长,事务边界太长会影响性能。`
### `Saga(*Eventuate* *Tram* *Saga*)`
`核心以上的业务系统可以采用补偿事务,补偿事务处理方面在30年前就提出了 Saga 理论,随着微服务的发展,近些年才逐步受到大家的关注。目前业界比较也公认 Saga 是作为长事务的解决方案。`
`Saga有两个方式:Orchestration(有中心协调者)和Choreography(无中心协调者)。`
- 1. `有中心协调者的Saga方式需要可能存在协调者本身失败的单点风险,但是能够方便减轻业务应用的开发量,能够形成Saga框架,由框架自己管理流程前进和回退。`
- 2. `Choreography需要使用事件概念,引入事件概念可能会增加业务应用开发的难度,除非业务应用时遵循DDD领域事件开发方式。这样,事务处理就是领域事件的处理方式了。
`最近一个项目中是用Eventuate Tram saga 的 Orchestration处理事务,也用Eventuate Tram框架处理领域事件。可以对比一下guava的eventBus。`
`https://developer.aliyun.com/article/725370`
`服务需要使用库来发送和接收消息。一种方法是使用消息代理的客户端库,但是直接使用这样的库有几个问题:`
- - `客户端库将发布消息的业务逻辑耦合到消息代理API。`
- - `消息代理的客户端库通常是非常底层的,需要多行代码才能发送或接收消息。作为开发人员,你不希望重复编写类似的代码。`
- - `客户端库通常只提供发送和接收消息的基本机制,不支持更高级别的交互方式。`
`更好的方法是使用更高级别的库或框架来隐藏底层的细节,并直接支持更高级别的交互方式。为简单起见,本书中的示例使用了我的Eventuate Tram框架。它有一个简单易用的API,可以隐藏使用消息代理的复杂性。除了用于发送和接收消息的API之外,Eventuate Tram还支持更高级别的交互方式,例如异步请求/响应和领域事件发布。`
`使用开源Eventuate框架,这个框架是为事务性消息、事件溯源和Saga量身定做的。之所以选择,是因为它与依赖注入和Spring框架不同,对于微服务架构所需的许多功能,目前开发者社区还没有广泛采用的框架。如果没有Eventuate Tram框架,许多演示代码必须直接使用底层消息传递API,这会使它们变得更加复杂并且会模糊重要的概念。或者使用一个没有被广泛采用的框架,这也会引起批评。`
`相反,这些演示代码使用Eventuate Tram框架,该框架具有隐藏实现细节的简单易懂的API。你可以在应用程序中使用这些框架。或者,你可以研究Eventuate Tram框架并自己重新实现这些概念。`
`Eventuate Tram还实现了两个重要机制:`
- - `**事务性消息机制:**它将消息作为数据库事务的一部分发布。`
- - `**重复消息检测机制:**Eventuate Tram支持消息的接收方检测并丢弃重复消息,这对于确保接收方只准确处理消息一次至关重要。
`Eventuate Tram框架为Java应用程序实现事务性消息。它提供了一个相对底层的API,用于以事务方式发送和接收消息。它还提供了更高级别的API,用于发布和使用领域事件以及发送和处理命令式消息。`
### `方法选择`
```
单机事务》基于消息的事务》基于补偿的事务》TCC事务
```
`因SAGA事务的形态需要配合较为明显的前端业务交互变更,个人建议在单一事务执行过程较长、存在较多子事务,并且无法使用基于消息的事务形态时使用。`
## `3. 开发角度`
`分为:存储层(respository),领域对象层(domain),服务层,接口web层。`
#### `领域对象层`
`https://www.jianshu.com/p/4551c0248289`
`我们的核心实体(Entity)和值对象(Value Object)应该在Domain层,不要把Entity的属性和行为分离到Domain和Service两层中去实现,即所谓的贫血模型,事实证明这样的实现方式会造成很大的维护问题。作为元模型中元素之一的实体本身就应该包含针对自身的行为定义。`
`一般返回对实体操作后,返回ResultWithDomainEvents,即包含结果,又包含事件。比如修改后,返回包含【实体】与【包含dto对象的事件】的组合结果。`
#### `服务层`
`通常持有持久化对象Repository,与事件发布对象EventPublisher。比如处理实体修改的操作,包括:`
- `根据id从存贮层得到实体`
- `对实体执行实体自身的操作`
- `对操作后返回的ResultWithDomainEvents进行处理,包括保存修改后的实体与发布事件操作。`
`服务层要么不返回结果,比如修改不返回,只是void。或者返回dto对象。`
`此外,还包含event处理包(领域事件处理用),command处理包(saga命令处理用)。`
#### `API`
`https://www.jdon.com/53930`
`遵循API优先方法,我们在开始编码之前先指定一个API。通过API描述语言,团队可以进行协作而无需执行任何操作。`
`API层包含领域事件对象,以及request对象,返回的vo对象。`
`使用openApi标准的yml接口规范文档,使用swagger-codegen-maven-plugin的maven插件,同时引用上面的对象,可以生成java API的代码框架。`
上一篇: 中国矿业大学深度解析:含院校档次级别介绍
下一篇: 西南大学深度解析:含院校档次级别介绍