安然写字的地方

关山难越,谁悲失路之人?萍水相逢,尽是他乡之客

0%

什么是领域

领域是指组织的业务范围以及在其中所进行的活动,也就是平台能力需要支撑的业务范围。因此,为了构建出平台能力,需要对业务领域进行深入的理解和设计。

在业务架构部分,将进行领域战略层级的建模,主要包括:“子域”、“领域对象”、“领域事件” 部分的设计。

领域事件识别

领域事件(Domain Event): 是领域专家关心的,在业务上真实发生的事件,这些事件对系统会产生重要的影响,如果没有这些事件的发生,整个业务逻辑和系统实现就不能成立。我们可以通过领域事件对过去发生的事情进行溯源,因为过去所发生的对业务有意义的信息都会通过某种形式保存下来。 比如:“用户地址已更新”、“订单已发货” 等领 域事件。
领域事件对系统常见的影响有:
• 对内

  • 产生了某种数据
  • 触发了某种流程或事情
  • 状态发生了某种变化

•对外

  • 发送了某些消息

目前比较常用的领域事件识别方法是“事件风暴 (Event Storming)”,
主要步骤如下:
• 邀请业务专家(或领域专家)和技术专家共同 参与事件风暴工作坊,其它参与角色按需补充。
• 明确和选择需要分析的业务场景。
• 确定起始事件和结束事件,事件以“XXX 已 YYY”的形式进行命名(对于英文版过去完成时的中文表达方法)。
• 根据场景和业务复述的复杂度,决定以时间线的哪个方向开始梳理事件(正向或逆向)【注】有些理论会要求从后往前识别。
• 以先发散再收敛的方式,按照时间线的先后顺序和并行关系,补充和完善领域事件。
• 使用“规则”抽象分支条件或复杂的规则细节,通过抽象降低分支复杂度,规则以“XXX规则” 的名词形式进行命名。
• 完成一遍事件梳理之后,通过问问题的方式,逆向检查(Reverse Check)事件流的逻辑合理性,例如:

  • 该事件真的需要在系统实现的时候考虑吗?
  • 该事件如果存在,那它的前提条件是什么?
  • 该事件如果要产生,那它的前一个事件必须是?
    • 重复以上步骤,迭代式的完成全部领域事件的识别。

领域对象识别

领域对象(Domain Object):是对业务的高度抽象,作为业务和系统实现的核心联系,领域对象封装和承载了业务逻辑,是系统设计的基础。
领域建模中重要的部分之一就是对“领域对象”及 领域对象之间关系的识别和设计。而领域对象识别 将基于前面领域事件识别的结果开展。
领域对象,通常包含(但不限于):
• 领域事件中出现了的名词;
• 如果没有信息系统,在现实中会看得见摸得着的事物(例如订单);
• 虽然在当前业务中看不见摸不着,但是可以在未来抽象出来的业务概念。

在领域驱动设计(Domain-Driven Design)中一般存在三类领域对象:
聚合根: 是领域对象的根节点,具有全局标识,对象其它的实体只能通过聚合根来导航;
如订单可以分为订单头和订单行,订单头是聚合根,它包含了订单基本信息;订单行是实体,它包含订单的明细信息,聚合跟所代表的聚合实现了对于业务一致性的保障,是业务一致性的边界。

实体: 是领域对象的主干,具有唯一标识和生命周期,可以通过标识判断相等性,并且是可变的,如常见的用户实体、订单实体;

值对象: 实体的附加业务概念,用来描述实体所包含的业务信息,无唯一标识,可枚举且不可变,如收货地址、合同种类等等。

业务架构只负责初步和整体识别领域对象,而对领域对象的分类(聚合根、实体、值对象)和战术层级的详细设计将在应用架构设计部分完成。

领域对象识别的主要步骤如下:
• 对每一个领域事件,快速识别或抽象出与该领域事件最相关(或隐含的)的业务概念,并将其以名词形式予以贴出。
• 检查领域名词和领域事件在概念和粒度(例如数量,单数还是集合)上的一致性,通过重命名的方式统一语言,消除二义性。
• 如果在讨论过程中,有任何因为问题澄清和知识增长带来的对于之前各种产出物的共识性调整,请不要犹豫,立刻予以调整和优化。
• 重复以上步骤,迭代式的完成全部领域对象的识别。

子域划分

子域(Subdomain):是对问题域的澄清和划分, 同时也是对于资源投入优先级的重要参考。比如: “订单子域”、“物流子域”等,子域的划分仍属于业务架构关注范畴。

核心域(Core Domain): 是当前产品的核心差异化竞争力,是整个业务的盈利来源和基石,如果核心域不存在那么整个业务就不能运作。对于核心域,需要投入最优势的资源(包括能力高的人), 和做严谨良好的设计。

支撑域(Supporting Subdomain): 解决的是支撑核心域运作的问题,其重要程度不如核心域,但具备强烈的个性化需求,难以在业内找到现成的解决方案,需要专门的团队定制开发。

通用域(Generic Subdomain): 该类问题在业内非常常见,所以很可能有现成的解决方案,通过购买或简单修改的方式就可以使用。

子域划分的主要步骤如下:

根据“每一个问题子域负责解决一个有独立业 务价值的业务问题”的视角出发,可以通过疑问句的方式来澄清和分析子域需要解决的业务问题,例如“如何进行库存管理?(英文描述 类似 How to…?)”。

利用虚线,将解决同一个业务问题的限界上下文以切割图像的方式划在一起,并以“XXX 子域” 的形式对每个子域进行命名。

根据三种类型的子域定义,共同结合业务实际 (或者参考设计思维中的电梯演讲),确定每个子域的子域类型

识别对象是否是聚合根

假设有一个业务系统,里面有项目,项目成员的概念,只有项目成员才能访问该项目。这个例子中,项目成员是否是聚合根?

假设这个系统是一个任务管理系统,支持用户创建项目,项目管理员可以设置项目的成员,项目成员可以在项目下创建任务,并把任务分配给其他成员。
在我看来,成员和任务,相对项目而言是平等的,他们都是项目下的业务数据,我们不能因为他们在某个项目下才有意义而把它们聚合在项目下

判断一个对象是否是独立聚合根,一般就是看创建该对象的场景是否是独立的。判断一个聚合根是否要聚合其他实体,主要看其是否关心其他实体的存在,以及从自身完整性的考虑。

场景是否是独立怎么理解的?

针对上面的例子,创建项目,添加项目成员,创建任务,分别是三个不同的业务场景,分别对应三个聚合根;项目聚合根不关心下面有哪些成员,哪些任务。项目的目的只是为了隔离业务数据,所以对于业务数据来说,项目只是一个分类标识

我们设计领域模型时不能以用户为中心作为出发点去思考问题,不能老是想着用户会对系统做什么;而应该从一个客观的角度,根据用户需求挖掘出领域内的相关事物,思考这些事物的本质关联及其变化规律作为出发点去思考问题。

领域模型是排除了人之外的客观世界模型,但是领域模型包含人所扮演的参与者角色,但是一般情况下不要让参与者角色在领域模型中占据主要位置,如果以人所扮演的参与者角色在领域模型中占据主要位置,那么各个系统的领域模型将变得没有差别,因为软件系统就是一个人机交互的系统,都是以人为主的活动记录或跟踪;比如:论坛中如果以人为主导,那么领域模型就是:人发帖,人回帖,人结贴,等等;DDD的例子中,如果是以人为中心的话,就变成了:托运人托运货物,收货人收货物,付款人付款,等等;因此,当我们谈及领域模型时,已经默认把人的因素排除开了,因为领域只有对人来说才有意义,人是在领域范围之外的,如果人也划入领域,领域模型将很难保持客观性。领域模型是与谁用和怎样用是无关的客观模型。归纳起来说就是,领域建模是建立虚拟模型让我们现实的人使用,而不是建立虚拟空间,去模仿现实。

参考

可观测性

首先可观测性的三个维度,logging metrics tracing
应用中是有自己的日志系统的,那么为了统计metrics,同一份数据需要记录两次吗?
如何划分这个界线?毫无疑问,Metrcis和Logging是有数据重叠的。我们可以认为Metrics是对可观测性指标的一种度量,例如请求数,函数调用次数等,但是对于Metrcis来说,它有着自己独特的属性【聚合】。另外,我们知道我们记录日志(Logging)是以事件为元数据,即记录当前发生了什么,这是Logging的关注属性。
在构想产品全链路追踪系统时,类似的问题再一次出现,我在记录Tracing数据的时候,或多多少会有Logging的数据,在Tracing中我认为重要的是链路数据指标属性,例如调用了哪些函数栈,该请求处理时间是多少等等,同样我们会在函数中记录得到了哪些请求,即Logging,但Tracing也有着自己独特的属性——请求范围。

关于监控指标的计算与实时统计,目前想到的两种方案

  • 一种是硬编码,侵入性的打点
    这种易于扩展,定制结构化的逻辑

  • 一种是根据日志来收集信息
    这种要求有规范化的日志格式,约束起来容易出问题

我觉得常规监控组件应该提供便利的SDK来进行指标的计算,底层是不是用打日志的方式统计实现的就不是那么重要了。

在过去,一个物理机器的状态确实可以通过几个监控指标描述,但是随着我们的系统越来越复杂,我们的观测对象正渐渐的从「Infrastructure」转到「应用」,观察行为本身从「Monitoring(监控)」到「Observability(观测)」。虽然看上去这两者只是文字上的差别,但是请仔细思考背后的含义。

参考

画图

在画时序图,类图时使用代码绘图会比手动画的效率更高,同时更容易表达思路

在画类图的应用中,最关注的应该是核心领域模型,以及各领域模型之间的依赖关系,属性、方法可以不用英文,便于理解就行

在画时序图时一定要分不同的场景case,这里我描述为用例,按用例画不同流程的时序图

看到的架构图经常会画一个四不像的流程图,把各用例的操作都包含进来,我觉得这不是一种好的实践,因为没有标准在里面,语义的表达也不够

代码绘图

反应式宣言

https://www.reactivemanifesto.org/

非反应式架构的问题(RPC/SOA)

  1. 同步等待,大量线程block,load高 资源利用率低
  2. 微服务化,在边界上划分开了,不能按照业务流程的依赖来并行,RT会累积
  3. 为了隔离RT累积引入Cache,超时时间,实现复杂

最近对这么一句话很有感触:
“reactive这味药吃多了容易伤身,真的用了就会回过头来求coroutine来救命。”
业务模块使用协程, 全局编排reactive才是合理思路。而不是为了背压全部上reactive

所以协程是否会取代反应式?
随着jvm loom项目和kotlin语言协程的推出,一些人认为反应式没有必要了,因为通过开销更小的协程来进行IO相关请求,同样可以提高CPU的利用率,但只是用协程,无法做到资源的最大化利用,并且无背压等支持,以及在处理实时数据流等业务场景中较为困难。如果反应式中基于线程池的调度转为基于协程池进行调度,这样可以很容易将只支持同步的SQL等阻塞IO改造为符合反应式的风格异步IO

优秀专题

对suspend的理解

suspend 标记只是一个提醒,作用在方法上只是表明“该方法会比较耗时,建议在协程中运行”
如果要实现程序的运行,需要将方法包装在这个结构中

1
2
CoroutineScope(Dispatchers.Default).launch{ }
CoroutineScope(Dispatchers.Default).async{ }

async/await做为库函数提供了
async的返回结果是个Deferred,类似CompletableFuture
kotlin提供了一个库可以将两者互转

详细资料

https://kaixue.io/kotlin-coroutines-1/
https://kaixue.io/kotlin-coroutines-2/
https://kaixue.io/kotlin-coroutines-3/

kotlin coroutine的实现

CPS,stackless

enode的整体线程模型的思考

技术选型采用了vert.x 实现异步反应式JDBC客户端,同时代码实现中存在大量的异步操作
如果不使用协程,会涉及到频繁的线程切换。由于C#中有好用的async/await语法
Java的实现要如何改造才好呢?这是个困扰我好久的问题,目前性能上不去我觉得和这个也有很大原因

另外,接口的实现,提供一个异步化的接口暴露出去是否合适,接口的设计需要优化,对应用中暴露的接口不应该出现Future之类的字样。

现有mailbox的实现,为了支持聚合根的批量提交消息,实现的有点复杂,我在想着能不能换成akka实现,同时使用disruptor
目前的策略,一个聚合根有一个command mailbox和一个 event mailbox,多个聚合根公用一批event commit mailbox,如果把这部分逻辑交给akka托管

消息返回类型选择

什么时候返回
三种模式

  • command消息发送完成
  • command消息执行完成
  • event消息消费完成

为什么一定要是顺序消息

对同一个聚合根的修改要路由到同一个队列中,如果rebalance或者不在同一个队列,会有并发冲突问题

间接模拟 async/await 语法糖

Java 中模拟async的执行时
如果是循环执行一个异步方法时,还要保证执行的顺序,这时可能就要借住递归的方式来实现

并发重复创建同一个聚合

两种情况
1.同一个命令并发执行创建同一个聚合根
在第一个命令执行未完成时,会直接抛出重复命令注册异常,如果第一个命令执行完成,则都幂等返回创建成功

2.不同命令并发创建同一个聚合根
开启批量提交时,检测出同时修改时,会抛出重复领域事件异常,错误返回

会出现一个创建成功,一个创建失败

published_version表的作用

记录对事件消息的消费进度

命令消息的顺序执行机制

command mailbox,类似actor,保证顺序消费

数据分类

  • 业务数据
  • 索引数据
  • 计算数据

设计思路

在做数据库设计时应该考虑几个点
索引和数据分离
数据和计算分离

做面向CQRS的业务系统设计是和这个思路完美契合的

场景分析

当我们的传统的MySQL无法满足 多样化的查询需求时,我们一般会引入lucene,elastic search类似的搜索引擎
那么在设计时有个问题,是将业务中所有的数据都异构到搜索引擎中,还是只把索引字段的数据存储进去只查询出id

看似简单的选择后面其实会完全走向不同的设计

一股脑的把业务数据全部异构到搜索引擎里面,我能想到的优点就是

  • 依赖数据源只有一个,不需要回源数据
  • 代码实现简单

反之带来的问题

  • 索引构建频繁,业务中任何数据的变更都要同步到搜索引擎中
  • 吞吐量低

但如果采用数据回源的方案,优点我想到的如下:

  • 索引和数据分离,搜索引擎索引的数据量下来了,吞吐量大,可针对性的横向扩容
  • get by primary key操作对于任何数据库都是极快的,为了极致性能时,很容易针对性的做缓存优化
  • 更容易和DDD中聚合根结合起来
  • 修改索引时,对业务数据不影响
  • 计算和数据分离,数据和索引分离

实际案例选型

es在数据量很大的情况下(数十亿级别)提高查询效率
性能优化离不开filesystem cache,比如说你现在有一行数据。id,name,age …. 30 个字段。但是你现在搜索,只需要根据 id,name,age 三个字段来搜索。如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 90% 的数据是不用来搜索的,结果硬是占据了 es 机器上的 filesystem cache 的空间,单条数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少。其实,仅仅写入 es 中要用来检索的少数几个字段就可以了,比如说就写入 es id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 es + hbase 这么一个架构。

hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回。

单元测试应该如何写

先上结论:

  • 不建议使用@Autowired直接属性注入,应使用构造器和setter方法注入

先抛出几个问题

  1. 按照应用的module划分模式,单测应该写在哪里?是每个module独立有自己的单测,还是在启动module中统一写单测?

  2. 单测时遇到的外部接口是否要采用mock的方式?

  3. 在Spring成为一种事实标准时,单测时一定要启动Spring容器吗? @Autowired的迷思?

简单来说下自己的思考

意图导向编程,测试用来表达接口设计契约
单测不是做测试,更是描述接口的职责

单测写在哪里主要是单测粒度的问题,每个module负责每个module的单测

单测时的外部依赖我们需要mock,减少我们对外部的依赖,保证程序的逻辑自治,外部依赖可替换

单测时一定要启动Spring容器吗,虽然Spring提供了很便利的测试工具,但每个测试都要启动容器,是不是带来了很多没必要的依赖,同时由于Autowired的便利性,我们可以很容易的将私有变量属性注入
这样就带来了一个问题,如果我们想要mock注入接口功能时,程序根本没有提供修改这个注入实例的方法
那为什么建议采用构造器注入呢,当我们想在这个类中增加功能时,这样可以思考下这个接口是不是应该在这个类中定义,会不会导致构造参数过多,而不是一味地增加私有属性变量

vert.x

vert.x 已经提供了很便利的 NetClinet,NetServer,这个EventBus Bridge提供的核心能力到底是什么呢?

Server和Client之间采用这种方式进行通信可以让Server端的代码更加清晰明了
Client和Server之间的交互模式也更简单一些:

  • 对于request-response,server简单地message.reply即可
  • 对于某些广播消息,client向多个server实例广播,或者是server向多个client广播,又或某些client也想接收另外的client向server的广播都非常简单和直观。
  • 通过注册地址,client也可以收到感兴趣的消息,不论是server发的,还是client发的

这里有个例子
https://github.com/foxgem/how-to/tree/master/vertx-tcp-eventbridge/src/main/groovy/foxgem

  • client向多个bridge send,则只有一个bridge可以收到
  • client向多个bridge publish,则都能收到
  • server和client之间的request-response
  • client注册要接受某地址的消息

vert.x 的EventBus有单机和cluster,bridge和cluster之间的区别又是什么?

EventBus 主要面向的是 verticles(类似actor)间的消息通信,cluster是为了将 EventBus 抽象为分布式的,同时提供了分布式网络下共识的能力

bridge 我理解则是通过跨进程的交互方式,让eventbus成为跨语言通信的 polyglot

EventBus虽然有tcp bridge,但是本意是用做verticles之间的通信用的

并不是用作客户端和服务器端之间的通信用的,正确的通信姿势应该是用vert.x的各种clients & servers

详细使用代码参见测试用例:
https://github.com/vert-x3/vertx-tcp-eventbus-bridge/blob/master/src/test/java/io/vertx/ext/eventbus/bridge/tcp/TcpEventBusBridgeTest.java

参考