DDD技能计划落地实践
1. 导言
从触摸范畴驱动规划的初学阶段,到完结一个旧体系改造到DDD模型,再到按DDD标准落地的3个的项目。关于范畴驱动模型规划研制,从开端的各种疑问到吸收各种先进的理念,现在在技能施行这一块现已底子比较老练。在既往经历中总结了一些在开发中遇到的技能问题和处理计划进行同享。
由于DDD的建模理论及办法论有比较老练的教程,如《范畴驱动规划》,这儿我对DDD的理论部分只做扼要回忆,假如需求了解DDD建模和根底的理论常识,请移步相关书本进行学习。本文首要针对咱们团队在DDD落地实践中的一些技能点进行同享。
2. 理论回忆
理论部分只做部分概要,关于DDD建模及根底常识相关,可参阅 Eric Evans 的《范畴驱动规划》一书及其它理论书本,这儿只做部分内容摘录。
2.1.1 名词
范畴及差异:范畴、子域、中心域、通用域、支撑域,限界上下文;
模型:聚合、聚合根、实体、值目标;
实体
是指描绘了范畴中仅有的且可持续改动的笼统模型,有ID标识,有生命周期,有状况(用值目标来描绘状况),实体经过ID进行差异;
每个实体目标都有仅有的 ID。咱们能够对一个实体目标进行屡次修正,修正后的数据和本来的数据或许会大不相同。比方产品是产品上下文的一个实体,经过仅有的产品 ID 来标识,不论这个产品的数据怎么改动,产品的 ID 一向坚持不变,它始终是同一个产品。
在 DDD 里,这些实体类一般选用充血模型,与这个实体相关的一切业务逻辑都在实体类的办法中完结。
聚合根
聚合根是实体,是一个根实体,聚合根的ID大局仅有标识,聚合根下面的实体的ID在聚合根内仅有即可;
聚合根是聚合复原和保存的仅有进口,聚合的复原应该确保完好性即整存整取;
聚合规划的准则
-
聚合是用来封装实在的不变性,而不是简略的将目标组合在一同;
-
聚合应尽量规划的小,首要由于业务决议聚合,业务改动聚合,尽或许小的拆分,能够防止重构,从头拆分
-
聚合之间的相关经过ID,而不是目标引证;
-
聚合内强共同性,聚合之间终究共同性;
值目标
值目标的中心实质是值,与是否有杂乱类型无关,值目标没有生命周期,经过两个值目标的值是否相同差异是否是同一个值目标;
值目标应该规划为只读形式, 假如任一特点发生改动,应该从头构建一个新的值目标而不是改动本来值目标的特点;
范畴事情
在事情风暴进程中,会识别出指令、业务操作、实体等,此外还有事情。比方当业务人员的描绘中呈现相似“当完结…后,则…”,“当发生…时,则…”等形式时,往往可将其用范畴事情来完结。范畴事情表明在范畴中发生的事情,它会导致进一步的业务操作。如电商中,付出完结后触发的事情,会导致生成订单、扣减库存等操作。
在一次业务中,最多只能更改一个聚合的状况。怎么一个业务操作触及多个聚合状况的更改,能够选用范畴事情的办法,完结聚合之间的解耦;在聚合根和跨上下文之间完结终究共同性。聚合内数据强共同性,聚合之间数据终究共同性。
事情的生成和发布:构建的事情应包含事情ID、时刻戳、事情类型、事情源等底子特点,以便事情能够无歧义地在不同上下文间传达;此外事情还应包含详细的业务数据。
范畴事情为已发生的业务,具有只读,不行改变性。一般接纳音讯为异步监听,处理的后续处理需求考虑时序和重复发送的问题。
2.1.2 聚合根、实体、值目标的差异?
从标识的视点:
聚合根具有大局的仅有标识,而实体只要在聚合内部有仅有的本地标识,值目标没有仅有标识;
从是否只读的视点:
聚合根除了仅有标识外,其他一切状况信息都理论上可变;实体是可变的;值目标是只读的;
从生命周期的视点:
聚合根有独立的生命周期,实体的生命周期从属于其所属的聚合,实体彻底由其所属的聚合根担任办理保护;值目标无生命周期可言,由于仅仅一个值;
2.2 建模办法
2.2.1 事情风暴
事情⻛暴法相似脑筋⻛暴,简略来说便是谁在何时根据什么做了什么,产⽣了什么,影响了什么事情。
在事情风暴的进程中,范畴专家会和规划、开发人员一同树立范畴模型,在范畴建模的进程中会构成通用的业务术语和用户故事。事情风暴也是一个项目团队共同言语的进程。
2.2.2 用户故事
用户故事在软件开发进程中被作为描绘需求的一种表达形式,并侧重描绘人物(谁要用这个功用)、功用(需求完结什么姿态的功用)和价值(为什么需求这个功用,这个功用带来什么样的价值)。
例:
作为一个“网站办理员”,我想要“计算每天有多少人拜访了我的网站”,以便于“我的赞助商了解我的网站会给他们带来什么收益。
经过用户故事分析会构成一个个的范畴目标,这些范畴目标对应范畴模型的业务目标,每一个业务目标和范畴目标都有通用的名词术语,而且逐个映射。
2.2.3 共同言语
在事情风暴和用户故事整理进程及日常评论中,会有越来越多的名词冒出来,这个时分,需求团队成员共同定见,构成名词字典。在后续的评论和描绘中,运用共同的称号名词来指代模型中的目标、特点、状况、事情、用例等信息。
能够用Excel或许在线文档等办法记载存储,标示称号,描绘和提取时刻和参加人等信息。
代码模型规划的时侯就要树立范畴目标和代码目标的逐个映射,然后确保业务模型和代码模型的共同,完结业务言语与代码言语的共同。
2.2.4 范畴差异及建模
DDD 内核的代码模型来源于范畴模型,每个代码模型的代码目标跟范畴目标逐个对应。
经过UML类图(经过色彩标示差异聚合根、实体、值目标等)、用例图、时序图完结软件模型规划。
2.3 整齐架构(洋葱架构)
整齐架构(Clean Architecture)是由Bob大叔在2012年提出的一个架构模型,望文生义,是为了使架构更简练。
整齐架构最首要准则是依靠准则,它界说了各层的依靠联络,越往里,依靠越低,代码等级越高。外圆代码依靠只能指向内圆,内圆不知道外圆的任何事情。一般来说,外圆的声明(包含办法、类、变量)不能被内圆引证。相同的,外圆运用的数据格局也不能被内圆运用。
整齐架构各层首要功用如下:
-
Entities:完结范畴内中心业务逻辑,它封装了企业级的业务规矩。一个 Entity 能够是一个带办法的目标,也能够是一个数据结构和办法调集。一般咱们主张创立充血模型。
-
Use Cases:完结与用户操作相关的服务组合与编列,它包含了运用特有的业务规矩,封装和完结了体系的一切用例。
-
Interface Adapters:它把适用于 Use Cases 和 entities 的数据转换为适用于外部服务的格局,或把外部的数据格局转换为适用于 Use Casess 和 entities 的格局。
-
Frameworks and Drivers:这是完结一切前端业务细节的当地,UI,Tools,Frameworks 等以及数据库等根底设施。
3. 落地实践
3.1 概述
在整个DDD开发进程中,除了建模办法和理论的学习,实践技能落地还会遇到许多问题。在多个项意图不断开发演进进程中,按部就班的总结了许多经历和小技巧,用于处理过往的缺憾和缺乏。走向DDD的路有千万条,这些仅仅其间的一些可选计划,如有疏忽还请纠正。
3.2 工程示例简介
现在咱们选用的是内核全体别离,如下图所示。
b2b-baseproject-kernel 内核模块阐明
其间: b2b-baseproject-kernel 为内核的Maven工程示例, b2b-baseproject-center为读写服务汇总的中心对外服务工程示例。
图3-1 kernel根底工程示例
内核Maven工程模块阐明:
1. b2b-baseproject-kernel-common 常用东西类,常量等,不对外SDK露出;
2. b2b-baseproject-kernel-export 内核对外露出的信息,为常量,枚举等,可直接让外部SDK依靠并对外,削减通用常识重复界说(可选);
3. b2b-baseproject-kernel-dto 数据传输层,便利app层和domain层同享数据传输目标,不对外SDK露出;
4. b2b-baseproject-kernel-ext-sdk 扩展点;(可选,不需求可直接移除)
5. b2b-baseproject-kernel-domain 范畴层等(也能够不差异子模块,按需差异即可);
(b2b-baseproject-kernel-domain-common 通用范畴,首要为一些通用值目标;
(b2b-baseproject-kernel-domain-ctxmain 中心范畴模型,可自行调整称号;
6. b2b-baseproject-kernel-read-app 读服务运用层;(可选,不需求可直接移除)
7. b2b-baseproject-kernel-app 写服务运用层;
b2b-baseproject-center 完结模块阐明
图3-2 center根底工程示例
center Maven工程模块阐明:
对外SDK
1. b2b-baseproject-sdk 对外sdk工程;
1.1 b2b-baseproject-base-sdk 根底sdk;
1.2 b2b-baseproject-core-sdk 写服务sdk;
1.3 b2b-baseproject-svr-sdk 读服务sdk;
根底设施
2. b2b-baseproject-center-common 常用东西类,常量等;
3. b2b-baseproject-center-infrastructure 根底设施完结层;
(b2b-baseproject-center-dao 根底设施层的数据库拜访层,也可不分,直接融合到infrastructure);
(b2b-baseproject-center-es 根底设施层的ES拜访层,也可不分,直接融合到infrastructure);
center服务层
4. b2b-baseproject-center-service center的业务服务层;
接入层
5. b2b-baseproject-center-provider 服务接入完结;
springboot发动
6. b2b-baseproject-center-bootstrap springboot运用发动层;
补白:对外SDK首要考虑适配CQRS准则,将读写分为两个独自的module, 假如感觉费事,也能够合并为一个SDK对外,用不同的分包阻隔即可。
内核和完结的相关
运用内核和详细完结运用别离的差异是由于前期由于有商业化衍生出了多版别开发。当然现在架构组是不主张一个内核多套完结的,而是主张一个内核加上一个主版别完结。防止由于多版别完结形成割裂,徒增开发和保护本钱,改为选用装备和扩展点来满意差异化需求。
现在咱们开发只坚持一个主版别,可是工程持续运用内核别离的办法,即一个内核+一个主版别完结。
长处:
-
内核和完结代码彻底阻隔,得到一个比较洁净存粹的内核;
-
虽万不得已不主张多版别完结,可是万一要支撑多版别,能够直接复用内核;
-
某种意义上,是一种更合理的别离,确保了内核和完结版别的别离,各自重视各自模块的中心问题;
缺陷:
- 联调本钱添加,每次改完需求本地install 或许推送到长途Maven库房;
根据以上原因,关于小工程不用做以上别离,直接在一个Maven工程中进行依靠开发即可 ,从许多示例教程也是引荐如此。
CQRS(指令与查询责任别离)
CQRS 便是读写别离,读写别离的首要意图是为了进步查询功用,一起到达读、写解耦。而 DDD 和 CQRS 结合,能够别离对读和写建模。
查询模型是一种非标准化数据模型,它不反映范畴行为,只用于数据查询和显现;指令模型履行范畴行为,在范畴行为履行完结后告诉查询模型。
指令模型怎么告诉到查询模型呢?假如查询模型和范畴模型同享数据源,则能够省却这一步;假如没有同享数据源,能够凭借于发布订阅的音讯形式告诉到查询模型,然后到达数据终究共同性。
Martin 在 blog 中指出:CQRS 适用于极少数杂乱的业务范畴,假如不是很合适反而会添加杂乱度;另一个适用场景是为了获取高功用的查询服务。
关于写少读多的同享类通用数据服务(如主数据类运用)能够选用读写别离架构形式。单数据中心写入数据,经过发布订阅形式将数据副本分发到多数据中心。经过查询模型微服务,完结多数据中心数据同享和查询。
范畴与读模型的联络与差异
范畴模型(以聚合根为仅有进口)是承载本体改变的中心,其是对业务模型的底子建模。若聚合根为每一个一般的人体,聚合根主键便是身份证ID。假定人人生而自由,不受人操控,那么当一个人接受到合理指令后进行自我特点改变,然后对外发送信息。
而视图层是人体和社会信息的投影,就如咱们的教育状况,工作状况,健康状况等相同。是对某个时刻对本体信息的投影。
视图由于根据音讯传达的特性,咱们的许多视图或许是推迟或许不共同的。事例:
1. 你现已阳了,而你的健康码仍是绿码;
2. 你现已成婚,而户口本仍是未婚;
3. 你的成婚证上聚合了你爱人的信息;
实践国际的不共同现已给咱们带来了许多费事和困扰,关于IT体系来说也是相同。视图的实时更新总是心旷神往,可是在分布式体系中面对许多应战。而为了消除范畴模型改变后各种视图层的推迟和不共同,就需求在音讯传达和更新时机上做一些优化。可是在业务处理上,仍是需求忍受必定程度的推迟和不共同,由于分布式体系是很难做到100%的准实时和共同性的。
3.3 问题及处理计划
3.3.1 范畴资源注册中心
布景
一般来讲,范畴模型不持有库房也不不持有其他服务,是一个比较。这就形成范畴模型在做一些验证的时分,仅能进行内存态的验证。关于rpc服务,以及触及一些重复性验证的状况,就显得力不从心。为了更好的处理这个问题,咱们选用了范畴模型注册中心,选用一个单例的类来持有这些服务;
那咱们在范畴模型中,从数据库从头加载回来的范畴模型,不需求经过spring进行数据封装,就能够直接运用所依靠的服务。
根据此,这些服务有必要是无状况的,经过输入范畴模型完结数据服务。
/**
* 租户注册中心
*
* @author david
* @date 12/12/22
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
@Setter
public class TenantRegistry {
/**
* 库房
*/
private TenantRepository tenantRepository;
/**
* 单例
*/
private static TenantRegistry INSTANCE = new TenantRegistry();
/**
* 获取单例
*
* @return
*/
public static TenantRegistry getInstance() {
return INSTANCE;
}
}
在范畴模型进行数据保存的时分,可用获取库房或许验证服务进行数据验证。
/**
* 保存数据
*/
public void save() {
this.validate();
TenantRepository tenantRepository = TenantRegistry.getInstance().getTenantRepository();
tenantRepository.save(this);
}
3.3.2 内核模块化
一般来讲,主站由于服务的客户量广,需求多样,导致功用及依靠服务也会很巨大。然后在进行商业化布置的时分,往往只需求其间10%~50%的才能,假如在布置的时分,全量的服务和范畴模型加载意味着需求装备相关的底层资源和依靠,不然或许发动反常。
内核才能模块化就显得尤为重要,现在咱们首要运用spring的条件加载完结内核模块化。如下:
/**
* 租户构建工厂
*
* @author david
*/
@Component
@ConditionalOnExpression("${b2b.baseproject.kernel.ability.tenant:true}")
public class TenantInfoFactory {
}
/**
* 租户运用服务完结
*
* @author david
*/
@Service
@ConditionalOnExpression("${b2b.baseproject.kernel.ability.tenant:true}")
public class TenantAppServiceImpl implements TenantAppService {
}
//其它相关资源相似,经过@ConditionalOnExpression("${b2b.baseproject.kernel.ability.tenant:true}") 进行动态开关;
这样在applicaiton.yml 装备相关才能的true/false, 就能够完结相关才能的按需加载,当然这是强依靠spring的根底才能状况下。
//appliciaton.yml 装备
b2b:
baseproject:
kernel:
ability:
tenant: true
dict: true
scene: true
可选进一步优化依靠
条件加载运用了spring的注解,某种意义上导致内核和spring进行了耦合。可是,项目中总有终极DDD患者,期望内核中最好连spring的依靠也去掉。这个时分,能够将spring的安装专门抽取到一个Maven的module作为starter,由这个starter担任spring的相关的注入和依靠进行适配。关于模块化加载装备,能够持续沿袭conditional的装备,实质上差异不大。
3.3.3 库房层diff实践(可选项)
本事例仅在运用联络型数据库,且为了提高更新时功用场景适用。假如能倾向于选用支撑业务的NoSQL数据库,那么本实践可直接略过。
假如不是受制于联络型数据库的愈加盛行的限制,在面向DDD开发之后,咱们或许更倾向于NoSQL数据库,能够将范畴目标以聚合根的为全体进行整存整取,这样能够大大的下降库房层存取耐久化数据的开发量。而现状是大部分项目都依靠于联络型数据库,故而许多数据仍然存在杂乱的数据库存储联络。
假如聚合根下相关多个实体,那么在更新的时分,比较简练的办法是全体掩盖,即便数据行没有发生改变。有时分为了提高数据库更新的功用,就需求按需更新,这时分就需求追寻实体目标是否发生改变。
对实体目标的改变追寻有两个办法:
A -> 保存更新前快照,运用反射东西深度对比值是否改变;
B -> 运用RecordLog 作为数据状况盯梢;
在过往项目中,A/B计划均选用过,A计划的代码侵入较少,可是需求保存更新前完好快照,运用反射状况下功用会略有影响。 B计划不需求坚持更新前完好快照, 也不用反射,可是需求在需求diff的实体目标中添加RecordLog值目标符号数据是新增、修正、或许未改变。
现在咱们首要选用B计划,在触及实体改变的进口办法,趁便调用RecordLog的更新办法,这样在库房层既能够判别是新增、修正、仍是没有发生改变。库房层在履行保存的时分,则可用经过recordLog值目标的creating, updating判别数据的状况。
/**
* 日志值目标,用于记载数据日志信息
*
* @author david
* @date 2020-08-24
*/
@Getter
@Setter
@ToString
@ValueObject
public class RecordLog implements Serializable, RecordLogCompatible {
/**
* 创立人
*/
private String creator;
/**
* 操作人
*/
private String operator;
/**
* 并发版别号,不用定以第三方传入的为准
*/
private Integer concurrentVersion;
/**
* 创立时刻,不用定以第三方传入的为准
*/
private Date created;
/**
* 修正时刻, 不用定以第三方传入的为准
*/
private Date modified;
/**
* 创立中
*/
private transient boolean creating;
/**
* 修正中
*/
private transient boolean updating;
/**
* 创立时构建
*
* @param creator
* @return
*/
public static RecordLog buildWhenCreating(String creator) {
return buildWhenCreating(creator, new Date());
}
/**
* 创立时构建,传入创立时刻
*
* @param creator
* @param createTime
* @return
*/
public static RecordLog buildWhenCreating(String creator, Date createTime) {
RecordLog recordLog = new RecordLog();
recordLog.creator = creator;
recordLog.created = createTime;
recordLog.modified = createTime;
recordLog.operator = creator;
recordLog.concurrentVersion = 1;
recordLog.creating = true;
return recordLog;
}
/**
* 更新
*
* @param operator
*/
public void update(String operator) {
setOperator(operator);
setModified(new Date());
setUpdating(true);
concurrentVersion++;
}
}
// 实体改变的时分,需求同步符号recordLog
public class TenantInfo implements AggregateRoot<TenantIdentifier> {
/**
* 失效数据
*
* @param operator
*/
public void invalid(String operator) {
setStatus(StatusEnum.NO);
recordLog.update(operator);
}
/**
* 发布
*
* @param operator
*/
public void publish(String operator) {
setBusinessStatus(TenantBusinessStatusEnum.PUBLISH);
recordLog.update(operator);
}
/**
* 保存到库房
*
* @param tenantInfo
*/
@Override
@Transactional
public void save(TenantInfo tenantInfo) {
TenantInfoPO tenantInfoPO = TenantInfoAssembler.convertToPO(tenantInfo);
RecordLog recordLog = tenantInfo.getRecordLog();
//创立diff判别
if (recordLog.isCreating()) {
tenantInfoMapper.insert(tenantInfoPO);
} else if (recordLog.isUpdating()) { //更新diff判别
UpdateWrapper<TenantInfoPO> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda().eq(TenantInfoPO::getTenantId, tenantInfoPO.getTenantId());
tenantInfoMapper.update(tenantInfoPO, updateWrapper);
}
//将范畴事情转换为taskPo, 并在一个业务之中保存到数据库,以便确保终究被消费
tenantInfo.publish(localTaskEventFactory.buildEventPersistenceAdapter(event -> TaskAssembler.tenantEventToTaskPO(event)));
}
3.3.4 读服务规划
一个完好的范畴服务,仅仅写入没有读取是不行的,只写不读会呈现信息黑洞,导致范畴改变无法被外部感知和运用。如前面所述,读服务是面向视图的,其需求的是简略检索(索引服务),宽表(冗余相关信息),摘要信息。且读服务不对源数据进行修正,无需进行加锁更重视呼应快速。
现在内核能相对标准化的读服务,首要针对聚合根进行底子的概况检索,如经过聚合根主键回来底子视图信息、列表检索等;其他个性化定制化的查询参数和呼应成果能够根据需求自行规划和扩展,假如是比较定制的查询服务,能够不用落地到内核之中。
在b2b-baseproject-kernel工程的 read-app 模块中,咱们界说了读服务的接口和束缚回来目标,则在完结的center工程中,首要完结底层的读库房和SDK接入层即可(可经过ES, 联络型数据库, redis 等来供给底层的检索服务)。
读服务接口:
/**
* 租户运用查询服务
*
* @author david
**/
public interface TenantInfoQueryService {
/**
* 经过租户code查询
*
* @param req
* @return
*/
TenantConstraint getTenantByCode(GetTenantByCodeReq req);
}
/**
* 经过租户编码查询租户信息恳求
*
* @author david
*/
@Setter
@Getter
@ToString
public class GetTenantByCodeReq implements Serializable, Verifiable {
/**
* 租户编码
*/
private String tenantCode;
@Override
public void validate() {
Validate.notEmpty(tenantCode, CodeDetailEnum.TENANT);
}
}
/**
* 示例租户读服务束缚接口
*
* @author david
* @date 4/15/22
*/
public interface TenantConstraint extends RecordLogCompatible {
/**
* 租户id
*/
Long getTenantId();
/**
* 租户id,编码
*/
Integer getTenantCode();
// ...
}
/**
* 租户运用查询服务内核完结
*
* @author david
**/
@Service
public class TenantInfoQueryServiceImpl implements TenantInfoQueryService {
//租户读库房
@Resource
private TenantReadRepo tenantReadRepo;
/**
* 经过租户id查询
*
* @param req
* @return
*/
@Override
public TenantConstraint getTenantByCode(GetTenantByCodeReq req) {
req.validate();
return tenantReadRepo.getTenantByCode(req.getTenantCode());
}
//...
}
3.3.5 范畴事情发布
假如不依靠binlog和业务性音讯组件, 为了确保范畴事情必定被发送出去,就需求依靠本地业务表。咱们将范畴目标保存和范畴事情发布使命记载在一个业务中得以履行。在范畴事情推送音讯中间件MQ中,在数据库保存结束后,先主动发送一次(容许失利),假如发送失利再等待定时调度扫描事情表从头发送。如下图所示:
一般状况下,范畴事情都是在业务操作的时分发生,此刻咱们将范畴事情暂存到注册中心。待入库的时分,在一个业务包裹中进行保存。发布者如下所示,假如聚合根需求运用此发布者事情注册服务,只需求完结此Publisher接口即可。由于内部运用了WeakHashMap 作为容器,假如当时目标不再被运用,之前注册的事情列表会被主动回收掉。
/**
* 描绘:发布者接口
*
*/
public interface Publisher {
/**
* 容器
*/
Map<Object, List<DomainEvent>> container = Collections.synchronizedMap(new WeakHashMap<>());
/**
* 注册事情
*
* @param domainEvent
*/
default void register(DomainEvent domainEvent) {
List<DomainEvent> domainEvents = container.get(this);
if (Objects.isNull(domainEvents)) {
domainEvents = Lists.newArrayListWithCapacity(2);
container.put(this, domainEvents);
}
domainEvents.add(domainEvent);
}
/**
* 获取事情列表
*
* @return
*/
default List<DomainEvent> getEventList() {
return container.get(this);
}
// 更多代码...略
}
简化计划
假如一些简略的运用,不需求运用MQ音讯行列进行事情中转,也能够将本地事情表的发送状况作为使命处理状况。这样能够简化一些网络开支,如在一个运用内,凭借guava的EventBus组件完结音讯发布-订阅机制。即简化为:订阅处理器假如悉数履行成功,才更新音讯表为已发送(也能够认为是已履行)。
在实践开发中,实践上咱们许多范畴事情都是根据此简化计划进行处理的,因范畴事情的部分处理功用简略,运用简化计划能节约许多开发时刻和代码量。
3.3.6 SAGA业务
概述
选用DDD之后,尽管仍是能够从运用层选用根底的业务性编程确保本地数据库的业务性。可是当处于微服务架构形式,咱们的业务常常需求多个跨运用的微服务协同,选用业务进行共同性确保就显得力所不及。
即便不选用DDD编程, 咱们过往现已开端选用Binlog(MySQL的主从同步机制)或许业务性音讯来完结终究共同性。在越来越盛行的微服务架构趋势下(运用资源的分布式特性),经过传统的业务ACID(atomicity、consistency、isolation、durability)确保共同性现已很难,现在咱们经过献身原子性(atomicity)和阻隔性(Isolation),转而经过确保CD来完结终究共同性。
处理分布式业务,有许多技能计划如:两阶段提交(XA)、TCC、SAGA。
关于分布式业务计划的优缺陷,有许多论文和技能文章,为什么挑选SAGA ,正如 Chris Richardson在《微服务架构规划形式》中所述:
-
XA对中间件要求很高,跨体系的微服务更是让XA力所不及;XA和分布式运用天然生成不匹配;
-
TCC 对每一个参加方需求完结(Try-confirm-cancel)三步,侵入性较大;
-
SAGA是一种在微服务架构中保护数据共同性的机制,它能够防止分布式业务带来的问题。经过异步音讯来和谐一系列本地业务,然后保护多个服务直接的数据共同性;
-
SAGA理论部分, 能够参阅:分布式业务:SAGA形式和Pattern: Saga
SAGA 理论
1987年普林斯顿大学的Hector Garcia-Molina和Kenneth Salem宣布了一篇Paper Sagas,叙述的是怎么处理long lived transaction(长活业务)。Saga是一个长活业务可被分解成能够交织运转的子业务调集。其间每个子业务都是一个坚持数据库共同性的实在业务。 论文地址:sagas
Saga的组成
-
每个Saga由一系列sub-transaction Ti 组成; (每个Ti是确保原子性提交);
-
每个Ti 都有对应的补偿动作Ci,补偿动作用于吊销Ti形成的成果; (Ti假如验证逻辑且只读,能够为空补偿,即不需求补偿);
-
每一个Ti操作在分布式体系中,要求确保幂等性(可重复恳求而不发生脏数据);
Saga的履行次序有两种:
-
T1, T2, T3, ..., Tn (抱负状况,直接成功);
-
T1, T2, ..., Tj, Cj,..., C2, C1,其间0 < j < n (向前康复形式,一般为业务失利);
-
Saga补偿示例: 假如在一个业务处理中,Ti为发邮件, Saga不会先保存草稿等业务提交时再发送,而是马上发送完结。 假如使命终究履行失利, Ti已宣布的邮件将无法吊销,Ci操作是补发一封邮件进行吊销阐明。
SAGA有两种首要的形式,协同式、编列式。
A 事情协同式SAGA(Event choreography)
把Saga的决议计划和履行