领域驱动设计DDD详解与战术建模落地

一、什么是DDD?

1.1、DDD的概念

Domain-Driven Design(领域驱动设计)它由Eric Evans在他的2003年出版的书籍《Domain-Driven Design: Tackling Complexity in the Heart of Software》中首次提出。DDD 核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性 。领域驱动设计,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。
因此DDD不是某一种设计模式、架构模式,而是一种软件开发方法论。 它从具体的较复杂和繁琐的业务问题出发,通过定义一致的领域语言,使用具体的设计工具,最终将问题转化为领域模型,从而给产品设计和技术设计提供重要支撑。

1.2、没有银弹

DDD不是万能的银弹,也存在着适用性和局限性
适用性:

  • 有一定的“领域”特征,并且在该领域内,核心域部分是存在的或者比较容易达成广泛一致认知的。
  • 具备一定的复杂性。
  • 业务是长期迭代的(非临时性的业务)。

局限性:

  • 临时的、一次性的或很简单的业务不适合,因为DDD的方法是有一定成本的。
  • 业务模式不成熟、变化很快时不适合DDD。
  • DDD并不擅长解决技术本身的难题,如性能、稳定性、大数据量等,遇到技术难题时还应结合其他的设计方式综合考虑,必要的时候要为技术难点做设计上的妥协。
  • 因此类似于很多成熟的业务模式的系统如:电商系统、CRM系统、采购系统等都是业务形态已经存在或很容易达成一致的场景,适合用DDD来梳理。
  • 反之如果想要搞一个临时的线上抽奖活动,就不适合了。也比如很多toC的创新业务在开始阶段,本身模式就不清晰,用DDD也是无法开展。

二、基本概念

2.1、领域对象-实体(entity)

传统的系统架构设计阶段,通常我们会将关注点放在数据上面,而不是领域上面。 这种设计风格在软件开发中,使数据库占据了主导地位,我们总是有限考虑数据的属性(对应数据库的列)和关联关系(外键关联),而不是富有行为的领域概念。这样做的结果是直接将数据模型反映在对象模型上,导致这些表示领域模型的实体中含有大量的getter、setter方法,也就是贫血领域模型,这不符合DDD的做法。
与传统数据模型设计优先不同,DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。传统数据模型不具备行为能力,而DDD的领域模型实体含有丰富的行为能力。

因此在DDD中理解领域对象的概念则变得尤为重要。
领域对象:指系统中的具有强业务属性的事物 ,例如业务中常出现的采购订单、员工、招聘候选人这类名词,需要我们在DDD建模过程中识别出来,并建立起对这些领域对象的属性定义、行为定义,同时它包括了实体、值对象。
在这里插入图片描述

实体: 在Eric中的书里描述Entity时,选用了指纹的图。而这其实就能完美说明了Entity的特点:唯一标识性

  • 两个实体即使其他的属性或行为完全一样,只要能代表唯一标识的ID不一样就不是一个实体。
  • 例如: 在一份采购订单中,即使其中的供应商相同、采购的品类,上传的附件、创建的时间等都相同。但是其采购订单的PO单号不同,那么它就不是同一个实体。
  • 实体是具有生命周期的,实体创建完后会被系统持久化,然后在某个时间,重新在系统中找到它,执行完某些业务属性变更后再次持久化。

生命周期最好能用有限状态机表示,如下是对考试、答卷实体的状态分析例子:
在这里插入图片描述
常见的实体: 订单、物流单、商品、用户。


2.2、领域对象-值对象(value object)

很多对象没有概念上的标识,它们描述了一个事务的某种特征,则这个对象则为值对象。因此两个值对象只要属性一样,那么就认为这两个值对象是指一样的东西。

  • 值对象和实体最本质的区别是值对象没有标识性的ID。
  • 常见的值对象如:订单的创建时间、工区的具体地址、商品的描述信息等。

区分出值对象的目的:

  • 避免细碎的实体属性,让实体模型更加清晰,属性更具有语义。
  • 减少实体的数量,引导团队思考某个对象建模为实体是否合适。
  • 为性能优化提供了更多的选择,因为值对象往往为数众多。eg: 如果每个电源插座都是一个实体,那么一做房屋设计中就可能含有上百个的不同的电源插座实体,但是假如设计成可互换的值对象,那么其实就只需要共享/复制同一个电源插座实例。在大型系统中这个效果可能会被放大数千倍。

2.2、聚合(Aggregate)

在 DDD 中,领域对象有实体和值对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。而能让这些个体协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。

聚合有一个聚合根(root)和上下文边界(boundary),这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,根则是聚合所包含的一个特定Entity。

  • 聚合根
    如果把聚合比作组织,那聚合根就是这个组织的负责人。 聚合根也称为根实体,它不仅是实体,还是聚合的管理者。 首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。最后在聚合之间,它还是聚合对外的接口人,以聚合根ID关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根ID关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。

如何在上下文边界内在众多对象中确定一个root?
假如现在有一个在采购系统中订单有一个验收的场景。验收单的组成由验收单头实体、以及n个验收单明细实体组成。它们之间的关系是1:n。
在这里插入图片描述

那么此时验收单头则是聚合根,验收的明细则是被聚合的实体。因为验收单头能唯一标识这个聚合根(含有验收单号)。这个验收单明细少了验收单头信息则变得没有意义(无法标识是哪个验收单)。

如何设计出好的聚合:

  • 设计小聚合: 如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性 ,让领域模型更能适应业务的变化。
  • 边界内的内容具有强一致性,边界之外最终一致: 在一个事务中只修改一个聚合实例。如果你发现边界内很难接受强一致,不管是出于性能或产品需求的考虑,应该考虑剥离出独立的聚合,采用**最终一致的(聚合间的一致性)**方式。这也是聚合能实现业务高内聚的原因
  • 通过唯一标识引用其他聚合 :聚合之间是通过关联外部聚合根ID的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度

2.3、工厂(factory)

创建一个对象或创建整个聚合时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用工厂进行封装创建。隐藏其创建的细节。因为假如让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或AGGREGATE的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合在这里插入图片描述

如何设计出好的工厂:

  • 工厂的创建方法应该都是原子的:如果FACTORY通过其接口收到了一个创建对象的请求,而它又无法正确地创建出这个对象,那么它应该抛出一个异常,或者采用其他机制,以确保不会返回错误的值。
  • FACTORY最好被抽象为所需的类型,而不是所要创建的具体类。

2.4、仓库(repository)

factory用来创建领域对象,而repository就是在生命周期的中间和末尾使用,来提供查找和持久化、检索对象并封装庞大基础设施的手段。

  • REPO vs DAO/DAL
  • 在使用repo的结构前我们一般取而代替的是直接使用DAO,DAL。DAO的核心价值是封装了拼接SQL、维护数据库连接、事务等琐碎的底层逻辑,让业务开发可以专注于写代码。但是在本质上,DAO的操作还是数据库操作,DAO的某个方法还是在直接操作数据库和数据模型。
  • 但是在领域驱动设计中,entity、value开始联结成聚合。管理聚合的生命周期,往往才是在DDD中经常会做的事。而Repository在DDD中作为领域模型和数据存储之间的桥梁它封装了对聚合根的检索和持久化逻辑,确保数据的一致性和完整性。 Repository通常与聚合根紧密相关,支持复杂的查询和持久化操作,这些操作与业务逻辑相关。

所以Eric提到在这个过程中,要注重的的是领域驱动设计的目标是通过关注领域模型(而不是技术)创建更好的软件。这与Robert在《整洁架构》里面的观点一致,领域模型是核心,数据模型是技术细节。假如研发构造一连串的sql,然后传给infra基础设施层的某个dal查询。再根据这些信息传给构造函数或者factory。这个过程很容易把我们的对象当做放置数据的容器(data/db model),整个设计就会回到数据处理风格。 处理的是技术,而不是模型。 因此引入Repository是尤为重要的,因为Repository的设计和实现与业务逻辑紧密相关,在repo处理domain model关注于模型本身,而在infra层的dal/dao层上处理data/model本身。
在这里插入图片描述

2.5、领域事件(domain event)

最早在eric书中其实未给出领域事件的正式定义,而是在该书出版之后提出来的。

在这里插入图片描述
因此当在做用户旅程或者场景分析时,我们要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。即当X发生时执行Y

因此领域事件基本位于多个聚合间,领域事件实现的是最终一致性。它可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。

  • 在实现上可以有一个整体的接口,这一部分主要是为了未来实现扩展可以用消息总线event bus实现通知到别的聚合的广播。
type DomainEvent interface {EventId() stringOccurredOn() int64
}

其中Id则为表示这个事件的唯一Id以及OccurredOn()这个事件所发生的事件。
简易的eventbus实现:

type EventBus struct {listeners map[string][]chan DomainEvent
}func NewEventBus() *EventBus {return &EventBus{listeners: make(map[string][]chan DomainEvent),}
}func (eb *EventBus) Subscribe(eventKey string, listener chan DomainEvent) {eb.listeners[eventKey] = append(eb.listeners[eventKey], listener)
}func (eb *EventBus) Publish(eventKey string, event DomainEvent) {for _, listener := range eb.listeners[eventKey] {listener <- event}
}

接着可以按照定义的描述在domain层中定义具体的对应聚合的事件:

type acceptanceEvent struct {eventId        intoccurrenceTime int64msg            string
}func (a *acceptanceEvent) EventId() int {return a.eventId
}func (a *acceptanceEvent) OccurredOn() int64 {return a.occurrenceTime
}

在repo层进行具体的发布:

func (a AcceptanceRepoImpl) Persist(agg *AcceptanceAgg) error {err := conn.TX(&a.ctx, func(tx *gorm.DB) error {// ... 持久化逻辑return nil})if err != nil {return err}// 发布验收单持久化事件a.publishPersistEvent(&acceptanceEvent{eventId:        strconv.FormatInt(agg.Acceptance.ID, 10) + "_" + strconv.FormatInt(time.Now().Unix(), 10),occurrenceTime: time.Now().Unix(),msg:            "验收单持久化",},)return nil
}

2.6、战略设计与战术设计

  • 战术设计:

    • 战术设计犹如使用一把精小的画笔在领域模型上描绘着每个细枝末节。用聚合将领域模型中的值对象、对象,在同一个领域边界内组织关联起来。
    • 而在这一过程中则会从技术视角出发,侧重于领域模型的技术实现完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和仓库、工厂、领域模型的充血行为等代码逻辑的设计和实现。
  • 战略设计:

    • 战略设计强调的是业务战略上的重点,如何按重要性分配工作,以及如何进行最佳整合。战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
    • 战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。 事件风暴过程会产生很多的实体、命令、事件等领域对象, 我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程, 而这个过程在eric中则被认为是产生统一通用语言的过程。
      在这里插入图片描述
  • 因此可以看出战略设计的难度是比战术的设计的难度更大一些的,因为它需要各方配合(产品、研发、业务专家等)不断地进行输出统一语言,划定领域的边界。而这在很多产品未达到一定规模是无法做到的。因为无法判断什么是这个产品最核心收益的部分(核心域),又或者哪些是通用子域等。

  • 但是战术设计虽然难度小一点,但是它在ddd也是重要的组成部分。没有战术设计的领域内基础,领域间的战略设计最多只能是纸上谈兵。而平常研发在ddd中发挥作用最大的则也是战术设计。因为它包括了聚合根、实体、值对象、领域服务、应用服务和仓库、工厂、领域模型的充血行为等代码逻辑的设计和实现。

三、分层设计

如今见到的大多的领域设计分层中大多是eric在书中提到的四层:

3.1、DDD四层架构

在这里插入图片描述

  • 用户界面层(或表示层): 负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
  • 应用层: 定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调
    任务,分配工作,使它们互相协作
    。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
  • 领域层(或模型层): 负责表达业务概念,业务状态信息以及业务规则。 尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心。
  • 基础设施层: 为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式。
  • 因此可以看出DDD分层与普通MVC分层的核心差别:隔离业务逻辑至领域层。 将业务逻辑隔离之后,我们可以专注于业务逻辑的开发和变动。 由于领域层不依赖其它层,所以改动其它层(如更换调用方式、存储加密)不会,也不应该影响到业务逻辑。
  • 并且每层只能与位于其下方的层发生耦合 ,否则会产生循环依赖
  • 层级之间职责更加分明,数据的调用流动更加单向。数据的混合调用往往是屎山代码的开端

分层之外的区别:

  • 实体拥有业务行为,贫血模型变成充血模型
  • 抽象数据模型,定义出每一个行为的聚合根和富集的Repo

带来的收益

  • 提升可读性: 充血模型的实体类由于包含业务逻辑,符合人类的思维模式,因此易于阅读与理解;将业务操作与数据库操作解耦,易测试
  • 提高一致性: 建立了一个比对象粒度更大的边界,聚集那些紧密关联的对象,形成了一个业务上的对象整体

3.2、应用服务 && 流程服务

DDD将原有的Service拆成了Application Service与Domain Service。从分层的概念上来看应用层主要做的是领域对象的协调任务,使它们互相协作,负责的是流程调度。而Domain层主要做的是表达业务概念,业务状态信息以及业务规则,负责业务规则处理。因此Application Service一般是领域间的调度流程集合,而Domain Service是业务规则逻辑处理的集合,Application Service是流程引擎,Domain Service是规则引擎。

假如这样还是觉得抽象可以这样思考:

  • 可以先按原有的思路先不区分Application、Domain Service。按照原有的MVC的思路合在一起,然后先抽离出这个service,在同一个领域内需要进行内存计算的逻辑。而这个就是就是Domain service的业务规则,而剩下的就为Application Service,负责调度domain service的方法进行流程的编排。
  • 假如原有的业务逻辑涉及到跨领域间的聚合,那么这相关的逻辑一定是写在Application Service层的,Domain Service的逻辑一定是只涉及在同一领域内的聚合。如:在这里插入图片描述

四、战术实践

4.1、系统分层

基于提到的目前系统分层架构为:在这里插入图片描述

4.2、代码分层

. 
├── cmd
├── conf
├── internal                                        // 整体业务逻辑入口
│   ├── userinterface                               // 用户接口层
│   │   ├── facade
│   │   │   └── lark_event_facade.go
│   │   └── handler
│   ├── application                                 // 应用层
│   │   ├── convert                                 // 转化层
│   │   │   ├── acceptance.go
│   │   │   └── attachment.go
│   │   └── service                                 // 应用服务
│   │       ├── acceptance_service.go
│   │       ├── acceptance_service_test.go
│   │       ├── lark_event_service.go
│   │       └── lark_event_service_test.go
│   ├── constants                                   // 常量
│   ├── domain                                      // 领域层
│   │   ├── acceptance                              // 具体的领域服务
│   │   │   ├── acceptance_service.go
│   │   │   ├── aggregate.go
│   │   │   ├── factory.go
│   │   │   └── repo.go
│   ├── infra                                       // 基础设施层
│   │   ├── conn                                    // 连接信息(redis、mysql...)
│   │   ├── dal                                     // dal/dao
│   │   ├── fsm                                     // 状态机
│   │   ├── gateway                                 // 网关(对外防腐层)
│   │   │   ├── lark_client
│   │   │   └── mdata_client
│   │   ├── model                                   // model集合
│   │   │   ├── bizmodel                            
│   │   │   └── dbmodel
│   │   └── mq                                      // 封装消息队列分发和消费
│   │       ├── consumer
│   │       ├── msg.go
│   │       └── productor
│   └── utils
├── handler.go                                      // rpc入口
├── idl.sh                                          // idl
├──  main.go
├── build.sh
└── README.md
  • domain层应该怎么划分?
    按照领域进行划分,领域内再按职责划分(聚合根、工厂、数据仓库、服务等…)。
  • convert 层的作用?
    用于进行外部数据与内部数据的转化兼容。如:
func AssembleAcceptance(ctx context.Context, req *procurement.ProcurementOrderAcceptanceReq) (acceptance *dbmodel.ProcurementOrderAcceptance,detailList []*dbmodel.ProcurementOrderAcceptanceDetail, err error) {// ...acceptance = &dbmodel.ProcurementOrderAcceptance{}if err = copier.Copy(acceptance, req); err != nil {logs.CtxError(ctx, "[CreateProcurementAcceptanceOrder] copy err:%v", err)return nil, nil, err}// ...return acceptance, detailList, nil
}// ConvertToOrderAcceptance 转换为前端需要的数据
func ConvertToOrderAcceptance(ctx context.Context, acceptance *dbmodel.ProcurementOrderAcceptance) (order *procurement.ProcurementOrderAcceptance, err error) {// ...order = &procurement.ProcurementOrderAcceptance{}if err = copier.Copy(order, &acceptance); err != nil {logs.CtxError(ctx, "[getProcurementOrderAcceptanceById] copy err:%v", err)return nil, utils.NewDataErr("转换验收单详情失败", err)}order.Id = acceptance.ID// ...return
}

这里用Assemble前缀标识代表数据是像内组装,向下传递。用ConvertTo前缀标识代表数据是向外转换,向上传递

4.3、从聚合出发

还是按照前面的例子:在采购系统中采购验收单由两部分组成:一个验收单主信息(验收单明细)与多个验收单明细。而验收明细又与一个订单明细一一对应。然后在创建一个验收单时,除了创建验收单头、验收单明细、还会联动的去计算更改订单明细的状态与订单的状态。
在这里插入图片描述
所以在这个过程中可以找出聚合内的实体对象应该有:验收头、验收明细、订单明细、订单头。那么如何去找聚合根呢?

  • 判断这个事件的主体是谁,客体是谁。谁是影响的主导来源,谁是被影响者

很明显在图中箭头来看,只有验收头是影响的来源,能通过引用链延伸到其他对象。 因此可以写出如下的聚合代码:

type AcceptanceAgg struct {// agg rootAcceptance           *dbmodel.ProcurementOrderAcceptanceAcceptanceDetailList []*dbmodel.ProcurementOrderAcceptanceDetailAttachmentList       []*dbmodel.ProcurementAttachment// 验收单所对应的订单明细、订单OrderDetailList []*dbmodel.ProcurementOrderDetailOrderList       []*dbmodel.ProcurementOrder
}

单单这样其实还不够,因为目前还是跟MVC的数据模型一致。实现行为的类与存储状态的类是分开的。假如在验收的过程中需要对属性进行检验计算,如验收金额不能小于等于0之类,这时根据mvc的数据模型的写法会面向过程过程的风格插入在service中。

  • 这种设计风格是高度面向过程的,Martin Fowler在《企业应用架构模式》将其称为事务脚本(transaction script pattern)模式。你只需要提供一个事务(需要执行的一段原子业务),然后去进行运行脚本(一组原子业务的编排方式-service)。
  • 所以这种方式与ddd方式有着强烈的差别:领域模型强调将业务逻辑封装在领域对象中,形成充血模型,以更好地反映业务概念和规则。而事务脚本则更侧重于流程和操作,可能没有充分利用面向对象的优势来组织业务逻辑
type AcceptanceAgg struct {// agg rootAcceptance           *dbmodel.ProcurementOrderAcceptanceAcceptanceDetailList []*dbmodel.ProcurementOrderAcceptanceDetailAttachmentList       []*dbmodel.ProcurementAttachment// 验收单所对应的订单明细、订单OrderDetailList []*dbmodel.ProcurementOrderDetailOrderList       []*dbmodel.ProcurementOrder
}// PersistValid 分读写校验(充血行为)
func (agg AcceptanceAgg) PersistValid() error {if agg.Acceptance.TotalAcceptanceAmount == 0 && agg.Acceptance.TotalAcceptanceQuantity == 0 {return utils.NewDataErr("验收单的数据不能为0", nil)}// ...其他数据校验return nil
}

扩展思考点:

  • 上图说了验收单头、验收单明细、订单头、订单明细等是实体,也就是领域对象。这里使用的是dbmodel,那么还是属于领域对象吗

    • 属于领域对象,但是此时的领域对象与数据模型进行了共用,最好还是拆分开来。可以创建一个一摸一样的实体,并且假如完全一摸一样,那么可以直接使用内嵌dbmodel的方式进行简化
    • 共用的缺点是dbmodel基本上是gen自动生成出来的,而在自动生成的文件上你无法做到充血。 并且一个是infra层一个是domain层,在分层上也不相同。在这里共用的原因是暂时可以在这聚合上去进行充血。因为可以说是聚合是领域对象的一种特殊形式,它通过将相关的实体和值对象组合在一起,形成了具有 明确边界和一致性规则的业务单元
  • 假如分页的基础上需要查询验收的信息光联到了申请单/商品的信息。并且这几个信息也能通过验收单去进行溯源关联进来此时需要把他们也加进来吗?

    • 可以,但是最好不要。因为一个好的聚合一定是高度精炼的,过多的实体一个复杂的聚合会变得不再内聚,并且难以维护。如果要做也应该在聚合能进行分布加载的时候做到。
    • 假如是分页操作设计到很多个表,但是其实每个表只用到几个字段,那么这个时候也可以不一定要使用领域模型。直接使用view封装成一个数据模型,然后在infra/dal层直接封装查询对view的方法。

4.4、Repo与Factory

Factory的内容则主要是New,这里使用OPTIONS设计模式来构造(也可以使用建造者模式等)。
aggregate.go:

type OptionFunc func(agg *AcceptanceAgg) errorfunc (agg *AcceptanceAgg) With(opts ...OptionFunc) (*AcceptanceAgg, error) {for _, opt := range opts {err := opt(agg)if err != nil {return agg, err}}return agg, nil
}
  • factory.go:
func NewAcceptanceAgg(opts ...OptionFunc) *AcceptanceAgg {agg := &AcceptanceAgg{}agg, _ = agg.With(opts...)return agg
}func WithOptionAcceptance(acceptance *dbmodel.ProcurementOrderAcceptance) OptionFunc {return func(agg *AcceptanceAgg) error {agg.Acceptance = acceptancereturn nil}
}func WithOptionAcceptanceDetailList(detail []*dbmodel.ProcurementOrderAcceptanceDetail) OptionFunc {return func(agg *AcceptanceAgg) error {agg.AcceptanceDetailList = detailreturn nil}
}
//...其他options方法
  • Repo则是用来加载聚合对象:
type AcceptanceRepo interface {// Load 加载验收agg(支持可选参数构造)Load(inputAgg *AcceptanceAgg, opts ...OptionFunc) (agg *AcceptanceAgg, err error)// Persist Persist 持久化存储验收信息Persist(agg *AcceptanceAgg) error// ...其他接口如LazyLoad、Store等
}type AcceptanceRepoImpl struct {ctx context.Context
}func NewAcceptanceRepo(ctx context.Context) AcceptanceRepo {return &AcceptanceRepoImpl{ctx: ctx,}
}func (a AcceptanceRepoImpl) Persist(agg *AcceptanceAgg) error {err := conn.TX(&a.ctx, func(tx *gorm.DB) error {err := agg.PersistValid()if err != nil {logs.CtxError(a.ctx, "[AcceptanceRepo-Persist] PersistValid has err:%v", err)return err}// 保存验收单表头if err := dal.GetDbFromCtx(a.ctx).Create(&agg.Acceptance).Error; err != nil {logs.CtxError(a.ctx, "[AcceptanceRepo-Persist] InsertAcceptance has err:%v", err)return err}// ..其他持久化逻辑return nil})return err
}// Load 加载验收agg,必须加载聚合根,其他作为options
func (a AcceptanceRepoImpl) Load(inputAgg *AcceptanceAgg, opts ...OptionFunc) (outputAgg *AcceptanceAgg, err error) {if inputAgg == nil {inputAgg = NewAcceptanceAgg()}// 由agg options控制加载outputAgg, err = inputAgg.With(opts...)// ... 在这里可以加入缓存逻辑,做cqrs直接读写分离return outputAgg, err}func WithLoadAcceptance(ctx context.Context, acceptanceId int64, acceptanceNO string) OptionFunc {return func(agg *AcceptanceAgg) error {params := &dal.QueryAcceptanceInfoParam{}if acceptanceId != 0 {params.AcceptanceIds = []int64{acceptanceId}}if len(acceptanceNO) != 0 {params.AcceptanceBusinessNos = []string{acceptanceNO}}// 获取验收单acceptanceList, err := dal.NewAcceptanceDB(ctx).GetAcceptanceListByQuery(&dal.QueryAcceptanceInfoParam{AcceptanceIds:         []int64{acceptanceId},AcceptanceBusinessNos: []string{acceptanceNO}})if err != nil {logs.CtxError(ctx, "[AggregateRepo-Load] GetAcceptanceListByQuery:%v", err)return err}agg.Acceptance = acceptanceList[0]return nil}
}
// ...其他options加载逻辑

4.5、在服务结束

接着编写application service、domain service验证下四层架构下的链路。请求从RPC层通过handler层转发。

func GetProcurementOrderAcceptance(ctx context.Context, req *procurement.GetProcurementOrderAcceptanceReq) (resp *procurement.GetProcurementOrderAcceptanceResp, err error) {resp = procurement.NewGetProcurementOrderAcceptanceResp()resp.BaseResp = base.NewBaseResp()// 填充订单详情resp, err = service.NewAcceptanceServiceImpl(ctx).GetProcurementOrderAcceptanceInfo(req.Id)if err != nil {utils.HandlerErr(ctx, resp.BaseResp, &err)return}return
}

应用服务层进行创建验收的流程调度。

type AcceptanceService interface {// GetProcurementOrderAcceptanceInfo 获取验收单详情GetProcurementOrderAcceptanceInfo(id int64) (resp *procurement.GetProcurementOrderAcceptanceResp, err error)
}func NewAcceptanceServiceImpl(ctx context.Context) AcceptanceService {return &AcceptanceServiceImpl{ctx: ctx,}
}func (a AcceptanceServiceImpl) CreateProcurementAcceptanceOrder(req *procurement.ProcurementOrderAcceptanceReq) (err error) {acceptance, detailList, err := convert.AssembleAcceptance(a.ctx, req)flag, err := preAcceptanceCheck(a.ctx, detailList)// ...进行前置校验,并且持久化验收信息txErr := conn.TX(&a.ctx, func(tx *gorm.DB) error {// 持久化验收单attachmentDbModels := convert.AssembleAttachmentList(req.AttachmentList, acceptance.ID, constants.AttachmentResourceTypeAcceptance)agg := acceptanceDomain.NewAcceptanceAgg(acceptanceDomain.WithOptionAcceptance(acceptance),acceptanceDomain.WithOptionAcceptanceDetailList(detailList), acceptanceDomain.WithOptionAttachmentList(attachmentDbModels))err = acceptanceDomain.NewAcceptanceRepo(a.ctx).Persist(agg)// 联动计算验收相关订单明细agg, err = acceptanceDomain.NewAcceptanceRepo(a.ctx).Load(agg, acceptanceDomain.WithLoadOrderDetailList(a.ctx))err = acceptanceDomain.NewAcceptanceService(a.ctx).CalOrderDetailsUpdate(agg)if err != nil {logs.CtxError(a.ctx, "[CreateProcurementAcceptanceOrder] CalOrderDetailsUpdate has err:%v", err)return err}err := dal.GetDbFromCtx(a.ctx).Save(agg.OrderDetailList).Errorif err != nil {logs.CtxError(a.ctx, "[CreateProcurementAcceptanceOrder] BatchSaveOrderDetail has err:%v", err)return err}// ...其他流程逻辑return nil})if txErr != nil {logs.CtxError(a.ctx, "[CreateProcurementAcceptanceOrder] Persist has err:%v", err)return txErr}return nil
}
  • Domain Service:
type AcceptanceService interface {// ...// CalOrderDetailsUpdate 计算验收单的更新时联动的订单明细变化CalOrderDetailsUpdate(agg *AcceptanceAgg) (err error)// ...
}type AcceptanceServiceImpl struct {ctx context.Context
}
func (s *AcceptanceServiceImpl) CalCancelOrderDetailsUpdate(agg *AcceptanceAgg) (err error) {// 进行恢复可验收数量/金额,已验收数量/金额orderDetailsMap := lo.GroupBy(agg.OrderDetailList, func(item *dbmodel.ProcurementOrderDetail) int64 {return item.ID})// 获取计算后的订单明细orderDetails := make([]*dbmodel.ProcurementOrderDetail, 0)for _, item := range agg.AcceptanceDetailList {// 从验收单流向订单,订单明细只会有有一条验收明细。orderDetail := orderDetailsMap[item.OrderDetailID][0]orderDetailRes, err := calCancelOrderDetailUpdate(s.ctx, orderDetail, item)if err != nil {logs.CtxError(s.ctx, "[CreateProcurementAcceptanceOrder] AcceptanceOrderEt has err:%v", err)return err}orderDetails = append(orderDetails, orderDetailRes)}agg.OrderDetailList = orderDetailsreturn nil}func calCancelOrderDetailUpdate(ctx context.Context, orderDetail *dbmodel.ProcurementOrderDetail,orderAcceptanceDetail *dbmodel.ProcurementOrderAcceptanceDetail) (res *dbmodel.ProcurementOrderDetail, err error) {// 进行恢复可验收数量/金额,已验收数量/金额orderDetail.AcceptableAmount = orderDetail.AcceptableAmount + orderAcceptanceDetail.AcceptanceAmountorderDetail.AcceptableQuantity = orderDetail.AcceptableQuantity + orderAcceptanceDetail.AcceptanceQuantityorderDetail.AcceptedAmount = orderDetail.AcceptedAmount - orderAcceptanceDetail.AcceptanceAmountorderDetail.AcceptedQuantity = orderDetail.AcceptedQuantity - orderAcceptanceDetail.AcceptanceQuantity// ..其他字段的计算res = orderDetailreturn
}

可以主要看到这一层application主要是把数据通过convert层进行转换成domain层需要的原始数据然后用factory进行创建,用repo进行持久化 ,然后调用domain层的服务进行业务规则计算,最后更新计算后的订单。

五、CQRS与性能

5.1、架构模式-CQRS

在DDD下,聚合联结了不同的的值对象与对象,成为了一个特殊形式下的领域对象。这个领域对象粒度会变的更大,此时加载这个聚合性能就会变得更大。并且在大多数场景下读负载与写负载并不对等,读的时候我可能只想读验收聚合中验收主信息而不想获取其附件信息、明细信息。而写的时候通常又是全量写入,达到聚合内数据的强一致。那么这个时候就可以从CQRS架构模式上寻找灵感。

CQRS(Command-Query Responsibility Segregation) 是一种读写分离的模式,从字面意思上理解Command是命令的意思,其代表写入操作;Query是查询的意思,代表的查询操作,这种模式的主要思想是将数据的写入操作和查询操作分开。
在这里插入图片描述

通过分离Query Service、Command Service。分离数据模型,在读模型下使用cache,按需加载方式等,更高性能的获取领域对象。
在这里插入图片描述

5.2、数据仓库性能优化 - 懒加载

  • 懒加载也叫延迟加载/按需加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。

通过懒加载的方式的方式可以加载只需要自己所需的一部分,而不需要加载无用的领域对象,到实际需要时再加载剩余部分,从量上进行性能的提高
而前面实现的则是通过options的方式,在构造函数的阶段就进行按需加载。或者用建造者模式进行创建:

// LoadAcceptanceAgg 是一个结构,它将用于加载AcceptanceAgg
type LoadAcceptanceAgg struct {acceptanceAgg *AcceptanceAgg
}// NewLoadAcceptanceAgg 创建一个新的LoadAcceptanceAgg实例
func NewLoadAcceptanceAgg() *LoadAcceptanceAgg {return &LoadAcceptanceAgg{acceptanceAgg: &AcceptanceAgg{},}
}// WithAcceptance 加载验收单信息
func (l *LoadAcceptanceAgg) WithAcceptance(ctx context.Context, id int64, no string) *LoadAcceptanceAgg {if err := WithLoadAcceptance(ctx, id, no)(l.acceptanceAgg); err != nil {//...}return l
}// WithAcceptanceDetailList 加载验收单明细列表
func (l *LoadAcceptanceAgg) WithAcceptanceDetailList(ctx context.Context) *LoadAcceptanceAgg {if err := WithLoadAcceptanceDetailList(ctx)(l.acceptanceAgg); err != nil {//...}return l
}// 其他With...方法类似...// Build 最终构建并返回AcceptanceAgg
func (l *LoadAcceptanceAgg) Build(ctx context.Context) (*AcceptanceAgg, error) {// 确保所有必要的步骤都已完成// 这里可以添加额外的构建逻辑return l.acceptanceAgg, nil
}

然后在加载的时候使用:

agg, err := NewLoadAcceptanceAgg().WithAcceptance(ctx), 1, "NOXXXXXX").WithAcceptanceDetailList(ctx).// 链式调用其他With...方法...Build(ctx)

这个方式也是JAVA @Builder注解的实现方式。

5.3、数据仓库性能优化 - 预先加载

  • 资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。

因此我们可以预先缓存未来可能需要用到的数据,缓存到读取性能更快的位置,如NOSQL/本地内存中,等到未来需要加载的时候使用。这也算是前面CQRS架构中提到的读负载的优化实践,走preload而不是直接走eager的方式。

  • 那么这个时候首先要考虑的是:需要缓存什么?以及缓存的设计的粒度是什么?

  • 在DDD的缓存设计的初期,应该缓存的粒度设计应该为领域实体对象。而不是整个聚合。 因为设计缓存整个聚合,维护/可用性上都会降低,那么因为聚合中的某一个对象更新。那么这一整个聚合的缓存都是脏的,需要进行回源等操作。而假如这个缓存粒度设计为对象级别的,那么只要更新单个缓存对象即可。然后在单个缓存对象的基础上可以再考虑做多级缓存,此时缓存整个聚合。
    在这里插入图片描述

因此可以通过lazyLoad + preLoad,两个维度上提高性能的缓存。

func WithPreLoadAcceptance(ctx context.Context, acceptanceId int64, acceptanceNO string) OptionFunc {return func(agg *AcceptanceAgg) error {cacheKey := fmt.Sprintf("%d-%s", acceptanceId, acceptanceNO)acceptanceCache := dal.GetCache(ctx, cacheKey)if acceptanceCache != nil {// 假设缓存中存储的是 JSON 字符串var acceptance *dbmodel.ProcurementOrderAcceptanceif err := json.Unmarshal(acceptanceCache, &acceptance); err != nil {return fmt.Errorf("unmarshal acceptance from cache failed: %v", err)}agg.Acceptance = acceptance} else {// 缓存未命中,从数据库加载err := WithLoadAcceptance(ctx, acceptanceId, acceptanceNO)(agg)if err != nil {return fmt.Errorf("load acceptance from database failed: %v", err)}// 将加载的数据存储到缓存acceptanceJSON, err := json.Marshal(agg.Acceptance)if err := dal.Cache(cacheKey, acceptanceJSON); err != nil {return fmt.Errorf("cache acceptance failed: %v", err)}// ...}return nil}
}

5.4、数据仓库性能优化-隐式读时复制

由于我们在存储的粒度往往面向的是聚合。那么这个时候为了在写的时候能够进一步的精细写的粒度,我们会希望在这背后的持久化机制上提供一些特殊的功能支持,来达到隐式的跟踪发生在每个持久化对象上的改变,从而在真正写的时候能从量上减少写的负载。在《实现领域驱动设计》第十二章则提出了,有两种方式可以达到这个目的,分别是隐式读时复制与隐式写时复制。

隐式读时复制(Implicit Copy-on-Read)[Keith & Stafford]:在从数据存储中读取一个对象时,持久化机制隐式地对该对象进行复制,在提交时,再将该复制对象与客户端中的对象进行比较。详细过程如下:当客户端请求持久化机制从数据存储中读取一个对象时,该持久化机制一方面将获取到的对象返回给客户端,一方面立即创建一份该对象的备份(除去延迟加载部分,这些部分可以在之后实际加载时再进行复制)。当客户端提交事务时,持久化机制把该复制对象与客户端中的对象进行比较。所有的对象修改都将更新到数据存储中

基于这个则是Snapshot的方案:当数据从DB里取出来后,在内存中保存一份snapshot,然后在数据写入时和snapshot比较。参考的实现有Hibernate/Entity Framework。当数据从数据库加载到内存中时,Hibernate会创建数据的一个快照,并在事务提交时比较这个快照和当前内存中的对象,以确定哪些数据发生了变化需要更新回数据库。
实现一个简单的demo,aggregate.go:

type AcceptanceAgg struct {// agg rootSnapshot             *AcceptanceAggAcceptance           *dbmodel.ProcurementOrderAcceptanceAcceptanceDetailList []*dbmodel.ProcurementOrderAcceptanceDetailAttachmentList       []*dbmodel.ProcurementAttachment// 验收单所对应的订单明细、订单OrderDetailList []*dbmodel.ProcurementOrderDetailOrderList       []*dbmodel.ProcurementOrder
}type AcceptanceDiff struct {AcceptanceDiffEnable     boolAcceptanceDiff           *dbmodel.ProcurementOrderAcceptanceAcceptanceDetailListDiff []*dbmodel.ProcurementOrderAcceptanceDetailAttachmentListDiff       []*dbmodel.ProcurementAttachment// 验收单所对应的订单明细、订单OrderDetailListDiff []*dbmodel.ProcurementOrderDetailOrderListDiff       []*dbmodel.ProcurementOrder
}
func (a *AcceptanceAgg) DetectDiff(new *AcceptanceAgg) *AcceptanceDiff {diff := &AcceptanceDiff{}if a.Snapshot == nil {return &AcceptanceDiff{AcceptanceDiffEnable: false,}}// ..diff操作给出结果return diff
}func (a *AcceptanceAgg) Attach() {// 强刷模式调用一次刷新一次当前的snapshota.Snapshot = deepCopy(a)
}func deepCopy(a *AcceptanceAgg) *AcceptanceAgg {return &AcceptanceAgg{// ... 对各属性进行深拷贝}
}

在repo层加载数据时加载snapshot,并在持久化时判断diff:

func (a AcceptanceRepoImpl) Load(inputAgg *AcceptanceAgg, opts ...OptionFunc) (outputAgg *AcceptanceAgg, err error) {if inputAgg == nil {inputAgg = NewAcceptanceAgg()}// 由agg options控制加载outputAgg, err = inputAgg.With(opts...)outputAgg.Attach()return outputAgg, err}
func (a AcceptanceRepoImpl) Persist(agg *AcceptanceAgg) error {diff := agg.DetectDiff(agg)if !diff.AcceptanceDiffEnable {// ...snapshot为空不启用diff直接全量load} else {// ...持久化diff的部分}return nil
}
  • 优点: 提供了一种相对简单的方式来跟踪和同步数据的变化,实现上较为简单。客户端直接操作数据的Snapshot,不需要通过委派对象,简化了数据模型。
  • 缺点: 在事务提交时,需要比较Snapshot和当前数据,如果字段较多,这个过程可能会比较耗时。并且在高并发的场景下,每个请求通常都会维护一个snapshot使用,内存占用容易过多。

5.5、数据仓库性能优化-隐式写时复制

隐式写时复制:持久化久化机制通过委派来管理所有被加载的持久化对象。在加载每个对象时,持久化机制都会为其创建一个微小的委派并将其交给客户端。客户端并不知道自己调用的是委派对象中的行为方法,委派对象会调用真实对象中的行为方法。当委派对象首次接收到方法调用时,它将创建一份对真实对象的备份。委派对象将跟踪发生在真实对象上的改变,并将其标记为“肮脏的”(dirty)。当事务提交时,该事务检查所有的“肮脏”对象并将对它们的修改更新到数据存储中。

基于此的实现有:当数据从DB里取出来后,通过weaving的方式将所有setter都增加一个切面来判断setter是否被调用以及值是否变更,如果变更则标记为Dirty。在保存时根据Dirty判断是否需要更新。
可参考Entity Framework中的使用,在Entity Framework中使用代理。这里可以写出一个以订单为例子的代理demo

// Entity 定义了领域实体的接口,所有领域对象都应该实现这个接口
type Entity interface {Save() error
}// Order 领域对象,实现了Entity接口
type Order struct {ID int// 其他字段...
}// Save 实现Entity接口的Save方法,持久化订单
func (o *Order) Save() error {// 持久化逻辑,例如写入数据库fmt.Printf("Order %d saved\n", o.ID)return nil
}// OrderProxy 订单的代理,实现了Entity接口
type OrderProxy struct {realOrder *OrderisDirty   bool
}// NewOrderProxy 创建一个新的订单代理
func NewOrderProxy(order *Order) *OrderProxy {return &OrderProxy{realOrder: order}
}// Save 实现Entity接口的Save方法,代理保存逻辑
func (p *OrderProxy) Save() error {if p.isDirty {return p.realOrder.Save()}// 如果没有更改,则不需要保存return nil
}// Modify 模拟对订单的修改,标记为脏
func (p *OrderProxy) Modify() {// 这里可以添加修改逻辑p.isDirty = true
}

每次领域实体都需要做数据更新时都通过OrderProxy去进行操作更新p.isDirty = true,这样就能知道哪些字段是做了更改。然后在实际更新时就能通过这个进行状态的diff。
优点: 性能更高,不需要维护snapshot,基本没有额外的成本开销。并且通过委派对象来管理数据,CoW可以简化并发控制机制,因为每个委派对象可以独立地跟踪数据的变更。
缺点: 实现成本会来的更高,当内嵌的对象比较复杂时,对于嵌套对象的字段更改也需要进行监听。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

透明度测试

&#xff11;、透明测试是用于处理哪种透明需求 在游戏开发中对象的某些部位完全透明而其他部位完全不透明,这种透明需求往往不需要半透明效果,相对比较极端&#xff0c;只有看得见和看不见之分比如树叶、草、栅栏等等。&#xff08;即一张图除了主要物体有颜色&#xff0c;其…

奇文网盘项目对应windows版本的中间件下载,otp,rabbitmq,postgresql,onlyoffice(在线预览编辑等)

之前的解压是百度网盘&#xff0c;要会员&#xff0c;油猴也无法下载&#xff0c;所有我下载之后给你们提供阿里云盘链接&#xff08;不限速&#xff09; 本次软件版本介绍&#xff1a; 01-otp_win64_24.1 02-rabbitmq-server-3.9.8 03-postgresql-9.6.23-2-windows-x64 0…

如何解决错误Given calling package android does not match caller‘s uid-学员提问

背景&#xff1a; 近来有学员反馈说wms课程中讲解的借壳Shell帮忙执行一些shell命令有问题&#xff0c;具体啥问题呢&#xff1f; 在ShellProvider的call方法加入如下代码&#xff1a; 目的就是想让shell帮我们执行一下settings值的写入&#xff0c;这里其实可以更加简单的set…

Linux 定时备份

背景&#xff1a;为防止数据丢失&#xff0c;要求每天备份一次 以达梦数据库为例&#xff0c;每天定时备份 1.填写备份脚本 vi db_day_backup.sh #!/bin/bash DIR$(cd $(dirname $0) && pwd) tarnamedata.tar_$(date %Y%m%d) cd $DIR if [[ $(find $DIR/ -name $tar…

腾讯提出一种新的针对风格化角色和逼真服装动画的生成3D运动转移方法,生成效果逼真!

来自腾讯XR视觉实验室的研究团队提出了一种创新的3D运动转移方法&#xff0c;专门针对风格化角色和逼真服装动画的生成。该方法能够将源动作准确地映射到目标角色上&#xff0c;同时考虑了角色身体的刚性变形和服装的局部物理动态变形。 与现有技术相比&#xff0c;这技术不仅…

4、Unity【基础】画线功能Linerenderer、物理系统Physics

文章目录 画线功能Linerenderer1、LineRenderer是什么2、LineRender参数相关3、LineRender代码相关思考1 请写一个方法&#xff0c;传入一个中心点&#xff0c;传入一个半径&#xff0c;用LineRender画个圆出来思考2 在Game窗口长按鼠标用LineRender画出鼠标移动的轨迹 核心系统…

Axure设计之单选框教程(中继器)

在Axure RP中&#xff0c;通过结合中继器的强大功能&#xff0c;我们可以轻松实现动态加载的单选框列表&#xff0c;不仅可以根据数据自动调整选项宽度&#xff0c;还能实时更新选中状态。本教程将引导你完成一个使用中继器制作动态单选框列表的项目&#xff0c;包括案例分析、…

Linux文件IO缓存

一、缓冲区大小对 I/O 系统调用性能的影响 总之&#xff0c;如果与文件发生大量的数据传输&#xff0c;通过采用大块空间缓冲数据&#xff0c;以及执行更少的 系统调用&#xff0c;可以极大地提高 I / O 性能 二、stdio 库的缓冲 当操作磁盘文件时&#xff0c;缓冲大块数据以…

树数据结构(Tree Data Structures)的全面指南:深度解析、算法实战与应用案例

树数据结构&#xff08;Tree Data Structures&#xff09;的全面指南&#xff1a;深度解析、算法实战与应用案例 引言 树数据结构&#xff08;Tree Data Structures&#xff09;作为计算机科学中的基石之一&#xff0c;以其独特的层次结构和分支特性&#xff0c;在众多领域发…

IIS中检测不到AspNetCoreModuleV2模块

安装了.net 2.2 的runtime&#xff08;运行时&#xff09;&#xff0c;但是在IIS中还是没有检测出来AspNetCoreModuleV2模块 解决方案&#xff1a; 其实问题点主要是选错了包&#xff0c;选成了x64&#xff0c;应该选择Hosting Bundle&#xff0c;这个是与IIS有关的。 之后下…

HW数通IA笔记2-网络参考模型

目录 零、本章主要内容 一、应用和数据 二、网络参考模型与标准协议 2.2 TCP/IP参考模型 2.3 TCP/IP常见协议 2.3.1 应用层 2.3.2 传输层 2.3.3 网络层 2.3.4 数据链路层 2.3.5 物理层 2.4 常见的协议标准化组织 三、数据的通信过程 零、本章主要内容 1、理解数据的…

SpringBoot集成kafka-生产者发送消息

springboot集成kafka发送消息 1、kafkaTemplate.send()方法1.1、springboot集成kafka发送消息Message对象消息1.2、springboot集成kafka发送ProducerRecord对象消息1.3、springboot集成kafka发送指定分区消息 2、kafkaTemplate.sendDefault()方法3、kafkaTemplate.send(...)和k…

关于elementui table组件 —— 竖向表格

前端模拟数据方式&#xff1a; html代码&#x1f447;&#xff1a; <template><el-table :data"tableData" style"width: 60%;margin-top:20px" stripe :show-header"false" border :row-style"rowStyle"><el-table…

MyBatis如何自定义项目中SQL日志

说明&#xff1a;用过MyBatis框架的同学们都知道&#xff0c;打印SQL日志&#xff0c;可以通过在application.yml配置文件中加入下面配置来设置&#xff1a; mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl但打印出来的SQL如下&#xff0c;丑陋…

机器学习/数据分析--通俗语言带你入门决策树(结合分类和回归案例)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 机器学习是深度学习和数据分析的基础&#xff0c;接下来将更新常见的机器学习算法注意&#xff1a;在打数学建模比赛中&#xff0c;机器学习用的也很多&a…

NVIDIA将在Hot Chips 2024会议上展示Blackwell服务器装置

NVIDIA 将在 Hot Chips 2024 上展示其 Blackwell 技术堆栈&#xff0c;并在本周末和下周的主要活动中进行会前演示。对于 NVIDIA 发烧友来说&#xff0c;这是一个激动人心的时刻&#xff0c;他们将深入了解NVIDIA的一些最新技术。然而&#xff0c;Blackwell GPU 的潜在延迟可能…

企事业单位数据资料防外泄如何实现?这5个小技巧等你来掌握!

企事业单位的数据资料防外泄是一项重要的任务&#xff0c;它关乎企业的核心竞争力和信息安全。 以下是五个实用的小技巧&#xff0c;可以帮助企事业单位有效地防止数据外泄&#xff1a; 1. 数据加密 技巧说明&#xff1a;通过对敏感数据进行加密处理&#xff0c;即使数据被非…

外贸管理软件一般都有哪些功能

外贸管理软件通常被设计来帮助国际贸易企业高效管理其业务流程。这类软件的功能多样&#xff0c;这里以神卓外贸管理软件为例&#xff0c; 以下是一些常见的核心功能模块&#xff1a; 客户关系管理 (CRM) 客户信息管理询盘与报价管理销售机会跟踪 订单管理 订单生成与处理发货…

Sparse Kernel Canonical Correlation Analysis

论文链接&#xff1a;https://arxiv.org/pdf/1701.04207 看这篇论文终于看懂核函数了。。谢谢作者

Azure OpenAI citations with message correlation

题意&#xff1a;“Azure OpenAI 引用与消息关联” 问题背景&#xff1a; I am trying out Azure OpenAI with my own data. The data is uploaded to Azure Blob Storage and indexed for use with Azure AI search “我正在尝试使用自己的数据进行 Azure OpenAI。数据已上传…