领域驱动设计&事件驱动框架&命令查询责任分离&测试驱动开发

这篇具有很好参考价值的文章主要介绍了领域驱动设计&事件驱动框架&命令查询责任分离&测试驱动开发。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

领域驱动设计: DDD

事件驱动框架: Event Driven Architecture

命令查询责任分离: CQRS(Command Query Responsibility Segregation)

测试驱动开发: TDD

先睹为快:架构图

java 领域驱动 参数名称 命令,后端,系统架构

 入口:entrypoint

入口是系统外部客户访问系统内部的端口。常见的入口如http, rpc, 命令行,外部消息(消费kafka,rocketmq或者zk, etcd的通知消息)。

入口的职责:解析外部输入,并将输入封装成命令,事件。然后调用服务层的MsgBus去处理。

命令与事件不同:命令需要其handler返回是否成功。而且一个命令唯一对应一个handler。事件则不需要返回成功,或者说失败后handler内部自己处理,一个事件可以有多个handler。事件是记录已经发的事,所以一般用过去式:如OrderCreated, OutOfResource;另一个概念消息,消息只是一个信息,可能转化成事件,也可能转化成命令。所以说Kafka, RocketMQ这些叫消息队列,而不叫事件队列或者命令队列。建议命令处理只返回简单的成功/失败,具体前端要展示处理后的结果,可以另发一个Query请求过来。

CQRS

一种区别对待写请求(命令)和读请求的设计模式。这样区分是因为写和读对系统的要求可能很不相同。

  • 读写本质不同

写请求一般会修改系统状态,而且其修改一般要穿过缓存,直接提交到数据库。一个系统的写请求一般远少于读请求。

读请求不修改系统状态,请求量也会非常大,这时候可以使用大量cache来提高性能。因此,读写分离一般能提高系统性能。

  • 优化读性能

甚至可以构建物化视图,即复制一份数据,按照查询的需求的数据结构来组织数据,并放到NoSQL数据库或者缓存中,提供快速查询。避免了在查询时还要进行复杂的表连接等耗费性能的重量级操作。

  • 更新视图,保证一致

在写请求完成后或者抛出事件,在专门的视图更新handler中更新视图(View)。

事件驱动的优势

无论是HTTP还是RPC还是外部消息,上层应用可能各不相同,但是统一抽象成事件或者命令后再交给MsgBus处理。这个解耦了入口应用层与服务层handler。同时在系统内部也可以抛出事件,这就把系统内外的处理入口集中并统一起来了。能实现这些因为中间引入了MsgBus和Event+Command。这样应用层和handler都可以自由变化,并灵活组合,而又互不阻碍。事件甚至是一种实体,他有唯一ID。可以持久化。防止事件丢失。

MsgBus

消息总线,维护了事件/命令和handler之前的关系。并分发事件到对应的handler。

UoW(Unit of Work)模式

一个Command或者Event在处理时,可能需要修改系统状态,而且在复杂又很深的调用链中,各种函数方法可能都要对同一个状态进程累进修改。UoW相当于一个请求的上下文状态,在处理请求前,构建UoW,一般是准备Repository, 然后把UoW带到handler以及各种需要修改状态的函数中去。当handler执行完成后,所有需要变更在状态都积累在UoW中了。此时可以在一个事务中提交修改。或者中途出现异常,则rollback,忽略修改。因此UoW是完成了一次事务。至于在处理过程中抛出的事件,则在提交事务后交由各事件handler处理。事件的处理并不在这次事务中。但是,UoW可以收集过程中的事件,如果事务回滚,则这些事件可以忽略。UoW可以在msg_bus,或者handler中或者由依赖注入模块来创建。他在handler结束后销毁。像python中UoW可以实现为ContextManager。

领域层

领域层包含领域服务,聚合,实体,值对象。这是DDD的核心概念。

  • 实体:有唯一ID和永久生命周期,修改其内部属性,实体依然是这个实体,而且会永久保存到Repository中。
  • 值对象:无ID,内部属性值标识了自己,生命周期短暂,一般不会修改其属性值,而是创建一个新的值对象。例如人是一个实体,有唯一ID(身份证),而人的姓名则是值对象,修复姓名,人还是那个人。只是重新创建了姓名这个值对象。
  • 聚合:聚集了多个领域对象,对于一些业务,涉及到修改多个领域对象,或者修改一批同一种领域对象,此时把这一批领域对象聚集起来,形成新的实体:聚合,此时就不能再单独操作聚合中的实体了。要通过聚合提供的方法来操作其中的实体。聚合越小越好。标准是更新一个聚合,系统仍然是一致的。也是聚合是一个事务,一种边界。
  • 领域服务:一些操作不适合放在聚合或者实体中,比如要修改一个聚合,但是会引用另一个聚合的数据,只是引用。

handler中会从仓储(repository)中获取必要的实体/聚合,并调用领域层的领域服务来完成业务操作。之后又把变更了的实体/聚合持久化到仓储中。因此,领域层一般不需要依赖仓储。

依赖倒置

handler也不依赖具体的仓储(repository). hander和具体仓储的实现(如MySQL仓储)都依赖抽象的仓储接口。这样可以灵活的更换各种仓储。其他三方库,如redis, kafka, etcd,通知服务(邮件,SMS)等也是一样的用依赖倒置原则。

领域事件

领域模型中可以抛出一些事件,如订单已经创建,资源已经分配,资源不足等。或者系统有异常抛出,也可以封装成事件。注意的是事件丢失或者后续处理失败并不会造成当前事务状态不一致。这也是判断哪些操作可以延后到事件处理中去的标准。这些事件并不需要立即调用MsgBus去处理。而是收集起来,可以暂时放到UoW中,或者暂时放在聚合/实体中,或者直接作为参数返回也行。这样,领域层也不会反向依赖MsgBus。事件一般是个值对象。至于要跟踪事件是否重复消费,则由MsgBus来负责。

基础服务层

定义好接口,使用依赖倒置原则解耦。接口的具体实现称为适配器。

仓储(repository):一个仓储一个聚合,一次事务,一个UoW

微服务划分与服务间通信

这涉及到DDD中的界限上下文。理论上一个聚合,或者说一个事务就能拆成一个微服务。当然,这样有可能让微服务太多,可以用现在流行的云函数来支持这么小级别的微服务。拆的小也有小的好处,比如资源分配上可以灵活,系统之间极大的解耦和。也有坏处,让系统运维起来复杂,理解起来困难。最好还是按业务界限划分。一种评判标准是:两个模块之间能异步通信,比如订单模块和仓库模块,订单下好后,发一个事件到消息队列,仓库模块异步处理即可。

事件驱动不是要禁止一切直接RPC:比如登录模块中要验证用户名密码,可能用到用户管理模块。现在很多做法是把登录抽象成SSO统一登录服务。这样业务服务中,只要RPC访问登录服务来校验token或者获取用户信息即可。可以看出,事件驱动的好处是解耦,缺点是异步,延时,因此,在线请求这种对延时要求极高的服务,还是得用RPC.不过可以看出,这些服务大多提供查询,校验,计算等服务。一盘不修改这些服务的状态。

系统内部并发与一致性

并发冲突

接入层HTTP, RPC在现实服务中都是并发的。当然可以单线程做事件循环。但是现代服务器都是多核心的。因此为提高性能,不可避免的使用多线程,多进程。甚至使用负载均衡器+集群的方式。这时,对同个实体的并发/并行修改不可避免。因此并发冲突可能导致状态被破坏,出现一张票卖个连个人等等问题。解决方法一般有两种:

  • 悲观事务枷锁: 像MySQL中种数据库支持ACID,事务加锁,在从repository中获取数据时使用select for update之类的语句,加锁后,其他并发将被阻塞。直到当前事务完成。显然性能低。
  • 乐观事务MVCC:像mongodb, etcd这类NoSQL数据库一般通过版本号控制并发,每个实体维护一个版本号,每次更新+1,真正提交事务的时候比较版本号是不是+1就能知道是不是冲突了。冲突了只能回滚了。这种做法乐观的认为冲突一般不会发生,并发度高,性能好。但是一但冲突,这次操作就白做了。

事件丢失&失败

异常情况发生时,如服务器崩溃,或者日志发布时重启服务器,可能会导致事务提交后,一些事件还没来得及处理就丢失了。最后导致了系统状态不一致。此时首先要考虑的是,是不是聚合没选对,导致其不是一个完整的事务。否则可以有以下处理方式:

事件也作为事务的一部分持久化到DB。重启后有专门线程扫描DB,恢复事件。注意事件处理的幂等性。这对于跨系统发送的事件通常是比较好的处理方式。

事后人工恢复:对于上述情况,人工介入,通常需要打足够多的日志,metrics等,后续人工分析介入。

事件失败:打日志,不妨碍其他事件继续处理

看这样一个例子,在游戏中,段位则模块判断玩家段位晋升了,并抛出了段位晋升事件。其他模块监听此事件,英雄模块可能要奖励玩家一个英雄,关卡模块要开放新的关卡。后续可能有其他模块奖励玩家物品或者金币。

实现方法:

直接在段位晋升后,依次调用英雄模块,关卡模块的函数。不抛出事件。显然,以后新模块加入又得改这里的代码。不符合开放封闭(OCP)原则。

引入新的领域服务来处理晋升:依次调用段位模块,英雄模块,关卡模块,最后提交。各模块是干净了,但是,领域服务这块儿代码不符合开放封闭(OCP)原则。

抛出事件:但是在段位保存前就处理完事件,把修改收集到UoW中。此时考虑的是不是聚合,或者领域服务抽象的不合理?

持久化事件:段位晋升后,把抛出的事件也作为段位模块的数据持久化到repository中。后台线程定时扫描并重放未处理的事件。如果这三个模块是独立的为服务,可能比较适合。因为事件投递到另消息队列和持久化到repository一般不在同一个DB中,没法进行事务操作。 或者得使用复杂的分布式事务,两阶段提交等复杂操作。

抽象出更高层的聚合:如玩家。玩家是一个聚合,自身带了段位,英雄,关卡等信息。理当作为一个聚合,同时持久化时,要以玩家为单位进行。需要注意,此时的事件处理确实需要在事务提交前进行。这里的MsgBus不再是上文提到的MsgBus。这是玩家这个聚合自己的MsgBus。这样系统中有两个MsgBus。或者进一步扩展,当聚合过于复杂时,为解耦各模块,有时也使用事件。

java 领域驱动 参数名称 命令,后端,系统架构

 全局事件中心与局部事件中心

这里虽然叫事件中心,实质与MsgBus实现一样。但是聚合内模块之间如果是命令的话,就直接调用函数。因此,只有这种一对多的情况用事件来解耦合。更复杂的可以组成事件树。这在游戏中组件模式下经常使用。

全局事件中心用于处理聚合外的事件,也就是聚合之间的事件。他在UoW事务提交后才执行。失败打日志,不影响聚合内一致性。

局部事件中心:处理聚合内事件,事件触发即执行。是聚合内的事件处理。在事务提交前,就执行完所有事件了。如果有失败,可以打日志。也可以抛出异常,中止整个操作。一般有失败,是代码BUG导致。需要修改代码。这里的事件中心已经不是为了事务间解耦合而使用事件了,而是为了聚合内部模块之间的解耦和。

如果需要投递事件,那应该是投递到哪个事件中心去呢?

  • 人工选择:提交事件时指定事件中心
  • 自动将局部事件全部也提交到全局事件。
  • 事件冒泡,即局部处理成功了就不往上层提交了。
  • 写个局部事件handler来决定哪些事件要提交到全局。

跨微服务事件:即全局中有些事件是发往外部系统的。可以统一发到消息队列中去,再由外部服务订阅。也可以直接调用外部服务接口(这样要求外部服务也可服务。否则要处理失败的情况,要求不高就打错误日志,人工恢复)

java 领域驱动 参数名称 命令,后端,系统架构

如下内容还在持续完善中.....

分布式系统,跨微服务的一致性:分布式事务

只读

微服务1只读不修改微服务2,3的状态,最后微服务1只修改自己的状态。这种就直接使用RPC吧,可以保证一致。

java 领域驱动 参数名称 命令,后端,系统架构

 先写

微服务1先修改微服务2,3的状态,成功后才能提交自己的修改:首先考虑是不是合并这三个微服务。不能的话考虑使用分布式事务,但也只能降低失败率,还会影响性能。

java 领域驱动 参数名称 命令,后端,系统架构

 后写

微服务1先修改自己的状态,然后才微服务2,3的状态:首先考虑是不是合并这三个微服务。然后考虑是使用消息队列解耦合。还不能的话考虑使用分布式事务,但也只能降低失败率,还会影响性能。

分布式事务7种解决方案:

分布式事务最经典的七种解决方案_分布式事务解决方案_Java烟雨的博客-CSDN博客

分布式事务解决方案:7种常见解决方案汇总_51CTO博客_分布式事务解决方案

七种分布式事务的解决方案 - 简书

TDD

测试驱动开发:在实现业务逻辑前。先写出测试用例。可能会觉得何必呢?增加了工作量。其实,在针对领域模型写测试用例时,其实时在梳理业务需求和功能。相当于用测试用例而不是文档来描述清楚需求。让后来者可以通过读测试用例就能知道系统的需求和功能。测试用例可以分为慢用例,如使用真实的数据库,和快用例,如使用Fake数据库。依赖道指使测试变得容易。可以由最初的对领域模型的测试,到最后的高层次的端到端测试逐步构建测试用例。文章来源地址https://www.toymoban.com/news/detail-760984.html

到了这里,关于领域驱动设计&事件驱动框架&命令查询责任分离&测试驱动开发的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 领域驱动设计——DDD领域驱动设计进阶

    进阶篇主要讲解领域事件、DDD 分层架构、几种常见的微服务架构模型以及中台设计思想等内容。如何通过领域事件实现微服务解耦?、怎样进行微服务分层设计?、如何实现层与层之间的服务协作?、通过几种微服务架构模型的对比分析,让你了解领域模型和微服务分层的作

    2024年01月15日
    浏览(52)
  • Spring 框架——事件驱动模型

    (1) Spring 事件驱动模型 是 Spring 框架中的一种编程模型,也被称为 发布/订阅模型 ,通过使用 观察者模式 和 事件机制 ,实现了组件之间基于事件的解耦和通信。在 Spring 中,事件驱动模型建立在应用上下文事件 ( ApplicationContextEvent ) 的基础上。 应用上下文事件 是一种用于

    2024年02月16日
    浏览(30)
  • 《领域驱动设计》:从领域视角深入仓储(Repository)的设计和实现

    一、前言 “ DDD设计的目标是关注领域模型而并非技术来创建更好的软件,假设开发人员构建了一个SQL,并将它传递给基础设施层中的某个查询服务然后根据表数据的结构集取出所需信息,最后将这些信息提供给构造函数或者Factory,开发人员在做这一切的时候早已不把模型看

    2024年02月08日
    浏览(80)
  • 领域驱动设计入门指南

    ​ 领域驱动设计(Domain-Driven Design,简称DDD)是一种软件架构风格,它强调在软件开发过程中紧密关注业务需求和领域知识。本文将简要介绍领域驱动设计的核心概念,帮助人开始学习和实践领域驱动设计。 什么是领域驱动设计? 领域驱动设计是一种软件开发方法,它侧重

    2024年02月11日
    浏览(38)
  • DDD领域驱动设计(六)

    领域对象需要资源存储。存储手段多样化,常见就是数据库,分布式缓存,localCache.资源库的作用,就是对领域的存储和访问进行统一管理对象。在抽奖平台中。通过下面这种方式组织资源库。

    2024年01月24日
    浏览(72)
  • 领域驱动设计四论

        经过多年的研究与思考,实践与总结,本人逐渐对 DDD 有所领悟,本文以一个较短的篇幅,提纲挈领地梳理出 DDD 的核心脉络,希望与各位做一探讨。 1776 年亚当斯密发表《国富论》,标志着经济学的诞生。2004 年,一本名为《领域驱动设计·软件核心复杂性应对之道》的

    2024年02月08日
    浏览(50)
  • 领域驱动设计之认知篇

    作为技术人,都有一个成为大牛的梦。 有些人可以通过自己掌握了比较底层、有深度、有难度的技术来证明自己的能力。 但对于绝大多数的应用研发工程师来说,其大部分的时间精力,会被消耗在读不懂、讲不清的屎山代码中,以及复杂多变的业务迭代中。很少会有需要去

    2024年02月08日
    浏览(43)
  • 领域驱动设计(Domain Driven Design)之建立领域模型

    在实际项目中,模型设计者往往过早陷入具体构造块类型的识别,比如实体、聚合、领域服务,而忽略了领域模型表达领域概念的目的。我们应该基于领域概念设计领域模型,然后再采用合适的模式降低领域模型的复杂度,进一步增加领域模型的表达能力。 领域模型的作用,

    2024年02月03日
    浏览(37)
  • 聊一聊对领域驱动设计中“领域”这个词语的理解与分析方法

    百度百科对领域的解释: 领域具体指一种特定的范围或区域 领域一般指的是业务的问题域,领域是有边界的,边界内,规定了我们要做什么,要做的范围,软件项目从开始到交付的过程中, 所有涵盖的业务,每个业务模块或者方向都有自己的业务范围和问题 比如做家装行业

    2023年04月14日
    浏览(38)
  • 领域驱动设计DDD实际项目落地最佳实践

    领域驱动设计(Domain Driven Design,简称:DDD)设计思想和方法论早在2005年时候就被提出来,但是一直没有被重视和推荐使用,直到2015年之后微服务流行之后,再次被人重视和推荐使用。 下面我来介绍一下DDD设计思想和方法论,同时结合我们在实际项目中应用总结和思考。 目录

    2024年02月08日
    浏览(65)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包