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

news2024/11/26 0:30:49

领域驱动设计: DDD

事件驱动框架: Event Driven Architecture

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

测试驱动开发: TDD

先睹为快:架构图

 入口: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。

基础服务层

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

仓储(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。或者进一步扩展,当聚合过于复杂时,为解耦各模块,有时也使用事件。

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

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

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

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

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

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

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

 

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

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

只读

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

 

 先写

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

 后写

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

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

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

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

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

TDD

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/478007.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法刷题|647.回文子串、516.最长回文子序列

回文子串 题目:给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。 具有不同开始位置或结束位置的子串,即使是由相同的字符…

【redis】redis分布式锁(三)自动续期

系列文章目录 【redis】redis分布式锁(一)手写分布式锁1.0~6.0 【redis】redis分布式锁(二)可重入锁设计模式 文章目录 系列文章目录前言一、CAPredis集群 APZooKeeper集群 CP 数据一致性Eureak集群 APnacos集群 AP 二、lua脚本脚…

1 认识仿真工具Packet Tracer【实验】【计算机网络】

1 认识仿真工具Packet Tracer【实验】【计算机网络】 前言推荐1 认识仿真工具Packet Tracer1.1账号注册与Packet Tracer软件下载1.1.1 下载1.1.2 安装 1.2 Packet Tracer界面简介1.2.1 总述1.2.2 详细 1.3网络拓扑构建与设备模块添加1.3.1如何往工作区中添加设备1.3.2添加连线1…

【计算机网络】1.1——因特网概述

因特网概述(了解) 网络、互联网和因特网 网络由若干结点和连接这些结点的链路组成 多个网络还可以通过路由器互连起来,互联网是"网络的网络“ internet 和 Internet internet(互联网或互连网)是通用名词 泛指…

Java 基础入门篇(四)——— 方法的重载与参数传递机制

文章目录 一、方法的定义二、方法的参数传递机制 ★2.1 基本类型的参数传递2.2 引用类型的参数传递 三、方法重载 一、方法的定义 方法的作用:封装一段代码的语法结构,可以被重复调用,以此提高代码的复用性,提高开发效率&#xf…

操作系统之线程

线程 一、产生原因 最开始的并发,只能更具切换进程的方式去交替执行进程来宏观上并发,但是切换进程需要切换执行进程的环境,有很大的系统开销,所以产生了线程,让其可以不用频繁的切换进程,使得线程成为CPU…

【论文笔记 fintune 】羊驼

1.论文 这些开发由两个关键组件提供动力:大型预训练语言模型(LM)和人工编写的指令数据。 1.1 背景 目前的训练太吃数据了,需要使用一些替代方法减少对数据的依赖有微调过的数据集效果远好于没有微调 用什么数据集微调呢&#x…

Collaborative Metric Learning(www 2017)

Background 传统基于MF的模型,将特征向量的点积作为预测结果,这存在一个很大的问题,即dot product 不满足三角不等式),这会导致学到的特征向量次优(难以捕捉用户细粒度的偏好)。本文提出了一种…

【Java笔试强训 20】

🎉🎉🎉点进来你就是我的人了博主主页:🙈🙈🙈戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔🤺🤺🤺 目录 一、选择题 二、编程题 🔥字符串反…

第十八章 协程

我们知道脚本都是继承自MonoBehaviour类,而其中的Update方法里面放置了大部分的游戏逻辑处理代码。Update方法是游戏循环的每一帧都去执行,这就要求我们的代码“无时无刻”不在处理所有的可能发生的情况,并做出相应的处理。如果我们想要完成“…

Vue之render函数

概述 render函数从名字上看就可以看出,它是一个用于渲染的函数,在Vue中我们要将我们写的界面展示到屏幕上时,使用的方式都是组件中的template:标签下编写内容后,由Vue将我们编写的界面渲染到屏幕上。而这个render函数就是比templ…

B. Divide Candies(数学 + 思维)

Problem - B - Codeforces Arkady和他的朋友们喜欢在一个n n的棋盘上玩跳棋。这个棋盘的行和列从1到n编号。 他的朋友们最近赢了一场比赛,所以Actady想用一些糖果来取悦他们。记得一则古老寓言(但不记得寓意)》,Arlady想给他的朋友们每个格子一个糖果组…

SpringMVC学习总结(一)SpringMVC简介入门案例

SpringMVC学习总结(一)SpringMVC简介/入门案例 一、SpringMVC简介 (一)什么是MVC MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分。 M:Model,模型层,指工程中的Ja…

手把手教你学会gitee的注册和代码的提交

欢迎来到壮壮的代码世界 gitee的注册和提交 一、gitee是什么?二、gitee怎么注册三、gitee的一些基本配置四、在gitee上创建远程仓库(存储代码的地方)五、clone远程仓库到本地电脑六、怎么把本地的代码推送到远程仓库去(git的三板斧…

jQuery操作

文章目录 1. jQuery基本介绍1.1 原理示意图1.2 快速入门1.2 什么是jquery对象1.3 dom对象转jQuery对象1.4 jQuery对象转dom对象1.5 jQuery选择器1.5.1 jQuery基本选择器1.5.2 jquery层次选择器1.5.3 基础过滤选择器1.5.4 内容过滤选择器1.5.5 可见度过滤选择器1.5.6 属性过滤选…

Vue(Vue脚手架)

一、使用Vue脚手架(Vue Cli) Vue官方提供脚手架平台选择最新版本: 可以相加兼容的标准化开发工具(开发平台) 禁止:最新的开发技术版本和比较旧版本的开发平台 Vue CLI🛠️ Vue.js 开发的标准工…

AWS VPC 配置指南:快速创建和设置你的虚拟私有云

文章目录 一、前言二、创建 VPC2.1 进入 AWS VPC 服务2.2 创建 VPC2.3 选择所要创建的 VPC 资源2.4 输入 VPC 名称2.5 设置 IPv4 CIDR block(IPv4 CIDR 块)2.6 选择可用区2.7 选择公有子网的数量2.8 设置 NAT 网关和 VPC 终端节点2.9 完成创建 VPC2.10 查…

python ast 详解与用法

目录 基本概念节点类型ast.Assignast.Nameast.Constantast.Callast.Attribute 结点的遍历ast源码示例 结点的修改示例 参考链接 基本概念 python 语言在编译前会解析为抽象语法树,整个文件可以看作一棵树,树上的每个节点就代表一条语句,中序…

计算机中找不到msvcp140.dll无法继续执行代码,解决方法

DLL是Dynamic Link Library的缩写,意为动态链接库。dll文件是电脑系统及软件运行的重要文件,电脑如果丢失dll文件,那么很多软件跟游戏都是无法运行的,msvcp140.dll丢失这个问题就有很多小伙伴遇到,小编今天就分享找不到…

山东专升本计算机第一章-计算机信息技术与计算机文化

计算机信息技术与计算机文化 计算机中的信息表示 数制及其转换 数制:用进位的原则进行计数数码:数制中表示基本数值大小的不同数字符号基数:一种数制所使用的数码个数位权:数码在不同位置的权值 数制的转换 • R进制转化为十进…