阿里面试:DDD 落地,遇到哪些 “拦路虎”?如何破局?

简介: 为每个子领域定义限界上下文(bounded context),限界上下文是一个清晰定义了领域模型的边界的范围。在限界上下文内,领域模型的概念是一致的,但不同限界上下文之间可以有不同的模型和语言。界限上下文,基本可以对应到 落地层面的 微服务。这就是 DDD 建模和 微服务架构, 能够成为孪生兄弟、 天然统一的原因。具体的方法论和落地实操,请参考 《第34章视频 DDD学习圣经》DDD 战略设计的第一步就是统一语言,也叫通用语言(UBIQUITOUS LANGUAGE),用于定义上下文。

本文的 原始地址 ,传送门

本文的 原始地址 ,传送门

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

DDD落地过程中遇到了哪些问题或者挑战?怎么解决的?

DDD 落地,遇到哪些 “拦路虎”?如何破局?

所以,这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V145版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

一:DDD落地的7个步骤

领域驱动设计(Domain-Driven Design,DDD)是一种软件开发方法,旨在帮助开发者更好地理解和设计复杂领域,并将领域模型直接映射到软件架构中。

要在项目中成功落地DDD,您可以采用以下一般性的步骤:

第一步:理解领域:

首先,您需要深入了解项目所涉及的领域。

这包括与领域专家合作,探索业务需求,收集和整理领域知识。

领域知识将成为您的领域模型的基础。

第二步:分解问题域、划分领域:

将领域划分为子领域(sub domains),识别出主要的业务概念和关系。

每个子领域可以有自己的领域模型,并负责特定的业务功能。

第三步:定义限界上下文:

为每个子领域定义限界上下文(bounded context),

限界上下文是一个清晰定义了领域模型的边界的范围。

在限界上下文内,领域模型的概念是一致的,但不同限界上下文之间可以有不同的模型和语言。

界限上下文,基本可以对应到 落地层面的 微服务。

这就是 DDD 建模和 微服务架构, 能够成为孪生兄弟、 天然统一的原因。

具体的方法论和落地实操,请参考 《第34章视频 DDD学习圣经》

第四步:定义统一语言

DDD 战略设计的第一步就是统一语言,也叫通用语言(UBIQUITOUS LANGUAGE),用于定义上下文
的含义。

如果定义统一语言,不同的团队,可以使用不同的 工具,可以使用思维导图、excel表格等等。

比如说,在尼恩的 《DDD学习圣经》中,就提到了 领域分析、四色建模、事件风暴 等工具。

第步:创建领域模型:

设计DDD中的常用模型,如实体(Entity)、值对象(Value Object)、聚合根(Aggregate Root)、仓储(Repository)、领域事件(Domain Events)等,以便更好地表达领域模型。

使用领域知识创建领域模型,这是DDD的核心。

领域模型是一种反映领域中实体、值对象、聚合根、仓储等概念的抽象模型。

可以使用面向对象编程来表现领域模型,并使用通用语言来描述领域概念。

第六步:领域模型映射:

将领域模型映射到代码中,可以使用对象关系映射(ORM)工具或手动编码。

确保领域模型的设计反映了领域知识和业务规则。

包括两个维度的映射:

  • 微服务层面的映射: BC到微服务的映射
  • 微服务内部的映射:领域对象的映射 : 如 entity 的映射

第七步:开发代码,并且测试领域模型:

通过DDD 一键代码生成工具,生成DDD 领域模型的骨架代码,并且完成领域业务代码的开发。

编写单元测试和集成测试来验证领域模型的正确性。

使用模拟对象(Mocks)等技术来隔离领域模型的测试。

第八步:持续演化:

随着项目的推进,持续改进和演化领域模型。与领域专家保持紧密合作,根据业务需求进行调整和扩展。

反馈循环:随着项目的演进,接受来自实际使用的反馈,不断改进领域模型和架构。

第九步:效能提升

领域驱动设计是一种强大的方法,可以帮助解决复杂领域中的问题,但它也需要投入时间和精力来构建和维护领域模型。

(1)使用 DDD工具 提升 效能:

考虑使用专门的DDD工具或框架,如EventStorming、CQRS(Command Query Responsibility Segregation)、Event Sourcing等,以更好地支持领域驱动设计。

(2)团队协作 提升 效能:

要成功落地DDD,需要一定的学习和实践,同时也需要团队的共识和支持。

在整个项目中,确保团队成员之间的良好沟通和协作,特别是与领域专家的沟通,以确保领域模型的准确性。

(2)文档和培训:

编写文档来记录领域模型和限界上下文,以帮助团队成员理解和使用它们。还可以提供培训以确保团队对DDD的实践有足够的了解。

(4)监控和性能优化:

在生产环境中监控应用程序,确保领域模型的性能和可伸缩性。根据实际需求进行性能优化。

二 DDD落地 遇到的问题

DDD(领域驱动设计)落地过程中通常会遇到以下几方面的问题或挑战及相应解决方法:

  • 2.1认知与理解层面 遇到的问题

  • 2.2设计与架构层面 遇到的问题

    1. 2.1限界上下文划分困难
  • 2.2.2聚合设计遇到困难

2.1 认知与理解层面 遇到的问题

团队成员对 DDD 理解不深或存在偏差。

DDD 是一种思想而非具体技术,与传统开发模式有差异,易导致理解困难,认为与传统分层架构类似,未把握其核心内涵3。

如果 解决认知与理解层面 的偏差?

  • 可以 多 组织专业培训与学习交流活动,请专家讲解或分享成功案例;
  • 可以 多开展团队内部讨论,对关键概念和原则进行深入探讨;
  • 可以 多 进行代码示例分析与实践练习,通过实际操作加深理解。

2.2 设计与架构层面 遇到的问题

  • 限界上下文划分困难

  • 聚合设计挑战

2.2.1 限界上下文划分困难

限界上下文划分困难 ,指的是: 难以准确界定不同限界上下文的边界,导致领域模型混乱,不同业务概念和逻辑交织,影响系统的可维护性与扩展性。

与业务专家紧密合作,基于业务流程、组织架构、业务规则等因素,梳理出清晰的业务边界;运用事件风暴等方法,识别关键业务事件和流程,以此确定限界上下文的范围。

如何 解决 限界上下文划分困难的问题呢?

(1) 深入理解业务(隐性的业务显性化)

  • 加强业务调研:与业务专家、领域用户进行充分沟通,通过访谈、问卷调查、实地观察等方式,全面了解业务流程、业务规则和业务目标。例如在电商系统中,要深入了解商品管理、订单处理、物流配送等各个环节的具体操作和相互关系,为限界上下文的划分提供充分的业务依据。
  • 绘制业务流程图:将业务流程以图形化的方式表示出来,清晰展示业务活动的先后顺序、参与角色和数据流向。这有助于发现业务中的不同逻辑板块,为划分限界上下文提供直观的参考。比如,在绘制银行贷款审批业务流程图时,可明显看出贷款申请、风险评估、审批决策等不同阶段,这些阶段可作为划分限界上下文的重要依据。
  • 建立领域知识图谱:梳理领域内的概念、实体及其关系,构建领域知识图谱,明确各个业务概念的边界和关联,帮助识别不同的限界上下文。如在医疗领域,可构建包含患者、医生、病历、诊断、治疗等概念的知识图谱,根据这些概念的紧密程度划分限界上下文。
  • 进行原型设计:通过快速搭建系统原型,能够帮助 识别 隐性流程/隐性的业务。

(2 ) 运用相关方法和原则

  • 基于业务能力划分:将具有相对独立、完整业务能力的部分划分为一个限界上下文。例如,在企业资源规划(ERP)系统中,采购管理、销售管理、库存管理等各自具备独特的业务能力,可分别作为不同的限界上下文。

  • 遵循单一职责原则:每个限界上下文应具有单一的业务职责,避免功能过于复杂和混杂。以在线教育系统为例,课程管理限界上下文负责课程的创建、编辑、发布等与课程相关的单一职责,而用户管理限界上下文则专注于用户的注册、登录、信息管理等职责。

  • 考虑业务变化频率:将业务变化频率相近的部分划分为同一个限界上下文。这样在业务需求发生变化时,可将影响范围控制在特定的限界上下文内,降低系统的维护成本。比如,在社交媒体平台中,动态发布和点赞评论功能的变化频率可能较高,而用户基本信息管理的变化频率相对较低,可将前者划分为一个限界上下文,后者划分为另一个限界上下文。

(3) 持续迭代优化

  • 进行原型设计:通过快速搭建系统原型,收集用户反馈,不断调整和优化。例如,在开发一款项目管理软件时,可先基于初步的限界上下文划分进行原型开发,让项目团队试用,根据反馈对不合理的划分进行改进。
  • 开展团队研讨:组织跨职能团队进行头脑风暴和研讨会议,让不同角色的人员从各自的专业角度对限界上下文的划分提出意见和建议。比如,开发人员、业务分析师、测试人员等共同参与讨论,从技术实现、业务逻辑、测试覆盖等多个维度审视划分方案,发现潜在问题并及时改进。
  • 在实践中演进:随着项目的推进和业务的发展,不断对限界上下文进行评估和调整。当出现新的业务需求或业务流程发生变化时,及时审视现有的限界上下文划分是否仍然合理,必要时进行重新划分或调整边界。例如,当电商平台新增了跨境业务时,可能需要对原有的限界上下文进行调整,增加与跨境物流、海关清关等相关的限界上下文。

2.2.2 聚合设计遇到困难

聚合设计遇到困难:指的是聚合根的选择与聚合边界的确定不易把握。聚合根过大导致性能问题,过小则逻辑分散;还存在聚合内部一致性维护困难的情况。

以下是针对聚合设计中常见问题的解决方案:

1. 聚合根过大导致性能问题

聚合根过大可能导致性能问题,例如加载和操作大量关联对象时的性能瓶颈。

解决方案

  • 拆分聚合:将大聚合拆分为多个小聚合,每个聚合只包含少量实体和值对象。例如,一个订单聚合可以拆分为订单头聚合和订单明细聚合。
  • 按需加载:采用懒加载(Lazy Loading)策略,仅在需要时加载关联对象。
  • 使用CQRS:对于复杂的查询需求,可以采用CQRS(命令查询责任分离)模式,将读模型和写模型分离,优化查询性能。

2. 聚合根过小导致逻辑分散

聚合根过小可能导致逻辑分散,难以维护。

解决方案

  • 合理划分聚合边界:根据业务规则和不变性(Invariants)来划分聚合边界,确保聚合内部的实体和值对象紧密相关。
  • 避免过度拆分:在拆分聚合时,避免过度拆分导致逻辑分散。聚合应尽量设计得小,但不应牺牲业务逻辑的完整性。

3. 聚合内部一致性维护困难

聚合内部一致性是聚合设计的核心目标,但维护一致性可能面临挑战。

解决方案

  • 明确不变性:在设计聚合时,明确聚合内部需要维护的不变性(即业务规则),并确保这些规则在聚合根的控制下。
  • 通过聚合根操作:所有对聚合内部对象的操作都应通过聚合根进行,避免外部直接访问。
  • 使用领域事件:对于复杂的业务逻辑,可以通过领域事件来解耦聚合之间的交互,同时维护最终一致性。

4. 聚合边界难以确定

确定聚合边界是聚合设计中的难点,尤其是当业务规则复杂时。

解决方案

  • 基于业务规则:聚合边界应根据业务规则和不变性来确定,而不是简单地根据对象的关联关系。
  • 事件风暴:通过事件风暴(Event Storming)方法,识别业务事件和聚合边界,确保聚合内的对象紧密相关。
  • 持续迭代:聚合设计不是一成不变的,可以根据业务需求和技术反馈进行调整。

5. 聚合之间的关联问题

聚合之间应尽量减少直接引用,避免耦合度过高。

解决方案

  • 通过ID引用:聚合之间应通过ID引用,而不是直接引用对象实例。
  • 服务层协调:在需要跨聚合操作时,可以通过服务层进行协调,而不是在聚合内部直接操作。

6. 性能优化

聚合设计需要在性能和一致性之间找到平衡。

解决方案

  • 缓存策略:使用缓存(如Redis)来减少对聚合根及其关联对象的频繁加载。
  • 异步处理:对于非关键业务逻辑,可以采用异步处理和最终一致性。

通过以上方法,可以有效解决聚合设计中的常见问题,确保聚合既能维护内部一致性,又能避免性能瓶颈和逻辑分散。

2.3 技术实现层面遇到的问题

2.3.1 Spring依赖注入问题

在DDD中,领域对象(实体和值对象)通常会包含业务逻辑和状态,但在Spring框架中,领域对象需要依赖其他服务或组件时,可能会遇到依赖注入问题。

例如,通过new关键字创建领域对象时,Spring容器无法注入依赖。

解决方案

  • 使用ApplicationContextAware:通过实现ApplicationContextAware接口,领域对象可以直接访问Spring容器中的bean,从而获得所需的依赖服务。但这种方式会引入对Spring容器的强耦合。
  • 将依赖作为参数传入:将依赖注入改为通过方法参数传递,这种方式更符合领域对象的独立性原则,有助于代码的可测试性和清晰性。

将依赖作为参数传入 的 Demo

这个demo 展示如何通过方法参数传递依赖,而不是直接在领域对象中注入Spring管理的服务。

场景描述

假设有一个Order领域对象,它需要计算订单的总价。

计算总价时,Order 需要调用一个DiscountService服务来获取折扣信息。

我们不希望在Order中直接注入DiscountService,而是通过方法参数传递。

1. 定义领域对象 Order

public class Order {
    private String orderId;
    private List<OrderItem> items;

    public Order(String orderId, List<OrderItem> items) {
        this.orderId = orderId;
        this.items = items;
    }

    // 计算订单总价,依赖 DiscountService 通过参数传入
    public double calculateTotalPrice(DiscountService discountService) {
        double total = items.stream()
                .mapToDouble(item -> item.getPrice() * item.getQuantity())
                .sum();

        // 调用 DiscountService 获取折扣
        double discount = discountService.getDiscount(orderId);
        return total * (1 - discount);
    }
}

2. 定义OrderItem 值对象

public class OrderItem {
    private String productId;
    private double price;
    private int quantity;

    public OrderItem(String productId, double price, int quantity) {
        this.productId = productId;
        this.price = price;
        this.quantity = quantity;
    }

    public double getPrice() {
        return price;
    }

    public int getQuantity() {
        return quantity;
    }
}

3. 定义 DiscountService 服务

public interface DiscountService {
    double getDiscount(String orderId);
}

@Service
public class DiscountServiceImpl implements DiscountService {
    @Override
    public double getDiscount(String orderId) {
        // 模拟根据订单ID获取折扣的逻辑
        return 0.1; // 10% 折扣
    }
}

4. 使用 OrderDiscountService

@Service
public class OrderService {
    private final DiscountService discountService;

    @Autowired
    public OrderService(DiscountService discountService) {
        this.discountService = discountService;
    }

    public void processOrder() {
        // 创建订单项
        List<OrderItem> items = List.of(
                new OrderItem("product1", 100.0, 2),
                new OrderItem("product2", 50.0, 1)
        );

        // 创建订单
        Order order = new Order("order123", items);

        // 计算订单总价,传入 DiscountService
        double totalPrice = order.calculateTotalPrice(discountService);
        System.out.println("Total Price: " + totalPrice);
    }
}

5. 启动应用程序

@SpringBootApplication
public class DddDemoApplication implements CommandLineRunner {
    @Autowired
    private OrderService orderService;

    public static void main(String[] args) {
        SpringApplication.run(DddDemoApplication.class, args);
    }

    @Override
    public void run(String... args) {
        orderService.processOrder();
    }
}

运行结果 运行程序后,输出如下:

Total Price: 225.0

将依赖作为参数传入 的 Demo 关键点

  1. 领域对象独立Order不直接依赖Spring容器,而是通过方法参数传入DiscountService
  2. 可测试性:可以轻松为Order编写单元测试,通过Mock DiscountService来测试calculateTotalPrice方法。
  3. 清晰性:依赖关系明确,代码更易读和维护。

总结:通过将依赖作为方法参数传递,可以避免领域对象与Spring框架的强耦合,同时保持代码的清晰性和可测试性。这种方式符合DDD的设计原则,是解决Spring依赖注入问题的推荐方法。

2.3.2 大聚合根的加载性能问题

问题

当一个聚合根包含大量关联实体或值对象,并且需要在应用程序中频繁加载和操作这些关联对象时,可能会导致性能下降2。

解决方法

  • 采用按需加载(Lazy Loading)策略,只在需要时加载相关对象;

  • 将大聚合根的关联对象分页加载;

  • 使用内存缓存如 Redis 等存储已加载的聚合根和关联对象;

  • 利用事件驱动架构,当聚合根变化时发布事件,让其他部分按需获取数据。

大聚合根的加载性能问题 demo

以下是一个示例,展示如何解决大聚合根的加载性能问题,涵盖按需加载、分页加载、使用 Redis 缓存以及事件驱动架构等解决方法。

1 定义实体类

假设我们有一个 Order 聚合根,它包含多个 OrderItem 关联实体。

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

// 订单聚合根
@Entity
public class Order {
   
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 按需加载关联对象
    @OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    public Long getId() {
   
        return id;
    }

    public void setId(Long id) {
   
        this.id = id;
    }

    public List<OrderItem> getOrderItems() {
   
        return orderItems;
    }

    public void setOrderItems(List<OrderItem> orderItems) {
   
        this.orderItems = orderItems;
    }
}

// 订单项实体
@Entity
public class OrderItem {
   
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String productName;
    private int quantity;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    public Long getId() {
   
        return id;
    }

    public void setId(Long id) {
   
        this.id = id;
    }

    public String getProductName() {
   
        return productName;
    }

    public void setProductName(String productName) {
   
        this.productName = productName;
    }

    public int getQuantity() {
   
        return quantity;
    }

    public void setQuantity(int quantity) {
   
        this.quantity = quantity;
    }

    public Order getOrder() {
   
        return order;
    }

    public void setOrder(Order order) {
   
        this.order = order;
    }
}

按需加载(Lazy Loading):在 OrderOrderItem 实体类中,使用 @OneToMany@ManyToOne 注解的 fetch = FetchType.LAZY 属性,确保关联对象只在需要时才会被加载。

2. 定义 Repository 接口

import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
   
}

public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
   
}

3. 使用 Redis 缓存

使用 Redis 缓存:在 OrderService 类中,使用 RedisTemplate 从缓存中获取订单信息,如果缓存中没有,则从数据库中获取并将其存入缓存。

分页加载

OrderService 类的 getOrderItemsByOrderId 方法中,模拟了分页查询关联对象的逻辑。

在实际应用中,可以使用 JPA 的分页查询方法。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class OrderService {
   
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public Order getOrderById(Long id) {
   
        // 先从缓存中获取
        Order order = (Order) redisTemplate.opsForValue().get("order:" + id);
        if (order == null) {
   
            // 缓存中没有,从数据库中获取
            Optional<Order> optionalOrder = orderRepository.findById(id);
            if (optionalOrder.isPresent()) {
   
                order = optionalOrder.get();
                // 将订单存入缓存
                redisTemplate.opsForValue().set("order:" + id, order);
            }
        }
        return order;
    }

    public List<OrderItem> getOrderItemsByOrderId(Long orderId, int page, int size) {
   
        // 这里简单模拟分页查询
        // 在实际应用中,需要使用 JPA 的分页查询方法
        Order order = getOrderById(orderId);
        List<OrderItem> orderItems = order.getOrderItems();
        int start = page * size;
        int end = Math.min(start + size, orderItems.size());
        return orderItems.subList(start, end);
    }
}

4. 事件驱动架构

定义了 OrderUpdatedEvent 事件,当订单更新时,通过 OrderEventService 发布该事件。

通过 OrderEventListener 监听该事件,当事件发生时,清除缓存中的订单信息,确保下次获取的是最新数据。

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

// 订单更新事件
class OrderUpdatedEvent extends ApplicationEvent {
   
    private final Long orderId;

    public OrderUpdatedEvent(Object source, Long orderId) {
   
        super(source);
        this.orderId = orderId;
    }

    public Long getOrderId() {
   
        return orderId;
    }
}

// 订单服务,实现事件发布
@Service
public class OrderEventService implements ApplicationEventPublisherAware {
   
    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
   
        this.eventPublisher = eventPublisher;
    }

    public void updateOrder(Order order) {
   
        // 更新订单逻辑
        // ...

        // 发布订单更新事件
        eventPublisher.publishEvent(new OrderUpdatedEvent(this, order.getId()));
    }
}

// 事件监听器
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class OrderEventListener {
   
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @EventListener
    public void handleOrderUpdatedEvent(OrderUpdatedEvent event) {
   
        // 当订单更新时,清除缓存
        redisTemplate.delete("order:" + event.getOrderId());
    }
}

5. 控制器类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/orders")
public class OrderController {
   
    @Autowired
    private OrderService orderService;
    @Autowired
    private OrderEventService orderEventService;

    @GetMapping("/{id}")
    public Order getOrder(@PathVariable Long id) {
   
        return orderService.getOrderById(id);
    }

    @GetMapping("/{id}/items")
    public List<OrderItem> getOrderItems(@PathVariable Long id, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) {
   
        return orderService.getOrderItemsByOrderId(id, page, size);
    }

    @PostMapping("/{id}/update")
    public void updateOrder(@PathVariable Long id) {
   
        // 简单模拟更新订单
        Order order = orderService.getOrderById(id);
        orderEventService.updateOrder(order);
    }
}

通过上面的方法,彻底解决大聚合根的加载性能问题。

2.3.3. 数据库与领域模型不匹配

领域模型和数据库模型之间可能存在不匹配,例如值对象需要序列化存储。

解决方案

  • 使用JSON字段或NoSQL数据库:对于复杂的值对象,可以将其序列化为JSON存储在关系数据库中,或者直接使用NoSQL数据库。
  • 转换器模式:通过Converter类实现领域对象与数据库实体的双向转换。

2.3.4. 复杂查询性能低下

在DDD中,领域模型的复杂性可能导致查询性能问题。

解决方案

  • 单独构建读模型:使用CQRS(命令查询责任分离)模式,将读写模型分离,通过Elasticsearch或物化视图优化查询性能。

2.3.5. 领域事件的可靠性问题

领域事件可能丢失或重复消费。

解决方案

  • 消息队列事务:通过Rocketmq 事务消息发布 领域事件, 确保领域事件的发布和消费具有事务性。
  • 幂等设计:在消费者端实现幂等逻辑,避免重复消费导致的问题。

2.4 团队协作与项目管理层面 遇到的问题

2.4.1 产研团队对业务理解不足的问题

业务研发团队缺乏对业务的深入了解,难以准确进行子领域和模块划分,把握数据归属等问题。

解决方法

  • 邀请业务专家对团队进行培训,分享业务知识和经验;
  • 产研团队参与业务流程梳理和需求调研,加强对业务的理解;
  • 建立业务知识库,方便团队成员随时查阅。

2.4.2 重构和业务演进的平衡的问题

业务持续演进,在进行 DDD 重构时,既要保证不影响现有业务,又要满足高优业务需求迭代 。

解决方法

  • 采用分阶段、小步快跑的方式进行重构;

  • 制定合理的重构计划,优先处理对业务影响小、收益大的部分;

  • 建立完善的测试和监控体系,及时发现和解决问题。

为何DDD如此之香?

DDD如此之香,那么多大厂对DDD如此痴迷, 背后 有深层次、根本性的原因

具体原因,参见尼恩在《DDD学习圣经》为大家深度总结的、下面的6点:

图片

DDD现在非常火爆,是有其巨大生产价值,经济价值的, 绝不仅仅是一套概念那么简单。

DDD未来大势所趋,是大家 明年3月面试,所需要必须掌握的 核心经验、 重点经验。

尼恩结合一个工业级的DDD实操项目,在第34章视频《DDD的学习圣经》中,给大家彻底介绍一下DDD的实操、COLA 框架、DDD的落地实操。并且指导大家写入简历, 帮助大家彻底穿透DDD。

《从0到1,带大家精通DDD》系列文章

除了本文,尼恩输出了一个 《从0到1,带大家精通DDD》系列,帮助大家彻底掌握DDD,链接地址是:

阿里DDD大佬:从0到1,带大家精通DDD

阿里大佬:DDD 落地两大步骤,以及Repository核心模式

阿里大佬:DDD 领域层,该如何设计?

极兔面试:微服务爆炸,如何解决?Uber 是怎么解决2200个微服务爆炸的?

阿里大佬:DDD中Interface层、Application层的设计规范

字节面试:请说一下DDD的流程,用电商系统为场景

DDD如何落地:去哪儿的DDD架构实操之路

DDD落地:从腾讯视频DDD重构之路,看DDD极大价值

DDD落地:从美团抽奖平台,看DDD在大厂如何落地?

美团面试:微服务如何拆分?原则是什么?

DDD神药:去哪儿结合DDD,实现架构大调优

DDD落地:从网易新闻APP重构,看DDD的巨大价值

DDD落地:从阿里单据系统,看DDD在大厂如何落地?

DDD落地:有赞的生产项目,DDD如何落地?

DDD落地:从携程订单系统重构,看DDD的巨大价值

DDD落地:京东的微服务生产项目,DDD如何落地?

DDD落地:从阿里商品域,看DDD在大厂如何落地?

240Wqps,美团用户中台, 如何使用DDD架构?

DDD落地:爱奇艺打赏服务,如何DDD架构?

相关文章
|
25天前
|
存储 SQL 算法
阿里面试:每天新增100w订单,如何的分库分表?这份答案让我当场拿了offer
例如,在一个有 10 个节点的系统中,增加一个新节点,只会影响到该新节点在哈希环上相邻的部分数据,其他大部分数据仍然可以保持在原节点,大大减少了数据迁移的工作量和对系统的影响。狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由”。在 3 - 5 年的中期阶段,随着业务的稳定发展和市场份额的进一步扩大,订单数据的增长速度可能会有所放缓,但仍然会保持在每年 20% - 30% 的水平。
阿里面试:每天新增100w订单,如何的分库分表?这份答案让我当场拿了offer
|
2月前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
5月前
|
存储 关系型数据库 MySQL
阿里面试:为什么要索引?什么是MySQL索引?底层结构是什么?
尼恩是一位资深架构师,他在自己的读者交流群中分享了关于MySQL索引的重要知识点。索引是帮助MySQL高效获取数据的数据结构,主要作用包括显著提升查询速度、降低磁盘I/O次数、优化排序与分组操作以及提升复杂查询的性能。MySQL支持多种索引类型,如主键索引、唯一索引、普通索引、全文索引和空间数据索引。索引的底层数据结构主要是B+树,它能够有效支持范围查询和顺序遍历,同时保持高效的插入、删除和查找性能。尼恩还强调了索引的优缺点,并提供了多个面试题及其解答,帮助读者在面试中脱颖而出。相关资料可在公众号【技术自由圈】获取。
|
1月前
|
算法 NoSQL 应用服务中间件
阿里面试:10WQPS高并发,怎么限流?这份答案让我当场拿了offer
在 Nacos 的配置管理界面或通过 Nacos 的 API,创建一个名为(与配置文件中 dataId 一致)的配置项,用于存储 Sentinel 的流量控制规则。上述规则表示对名为的资源进行流量控制,QPS 阈值为 10。resource:要保护的资源名称。limitApp:来源应用,default表示所有应用。grade:限流阈值类型,1 表示 QPS 限流,0 表示线程数限流。count:限流阈值。strategy:流控模式,0 为直接模式,1 为关联模式,2 为链路模式。
阿里面试:10WQPS高并发,怎么限流?这份答案让我当场拿了offer
|
2月前
|
人工智能 缓存 Ubuntu
AI+树莓派=阿里P8技术专家。模拟面试、学技术真的太香了 | 手把手教学
本课程由阿里P8技术专家分享,介绍如何使用树莓派和阿里云服务构建AI面试助手。通过模拟面试场景,讲解了Java中`==`与`equals`的区别,并演示了从硬件搭建、语音识别、AI Agent配置到代码实现的完整流程。项目利用树莓派作为核心,结合阿里云的实时语音识别、AI Agent和文字转语音服务,实现了一个能够回答面试问题的智能玩偶。课程展示了AI应用的简易构建过程,适合初学者学习和实践。
129 22
|
3月前
|
存储 NoSQL 架构师
阿里面试:聊聊 CAP 定理?哪些中间件是AP?为什么?
本文深入探讨了分布式系统中的“不可能三角”——CAP定理,即一致性(C)、可用性(A)和分区容错性(P)三者无法兼得。通过实例分析了不同场景下如何权衡CAP,并介绍了几种典型分布式中间件的CAP策略,强调了理解CAP定理对于架构设计的重要性。
172 4
|
4月前
|
存储 NoSQL 算法
阿里面试:亿级 redis 排行榜,如何设计?
本文由40岁老架构师尼恩撰写,针对近期读者在一线互联网企业面试中遇到的高频面试题进行系统化梳理,如使用ZSET排序统计、亿级用户排行榜设计等。文章详细介绍了Redis的四大统计(基数统计、二值统计、排序统计、聚合统计)原理和应用场景,重点讲解了Redis有序集合(Sorted Set)的使用方法和命令,以及如何设计社交点赞系统和游戏玩家排行榜。此外,还探讨了超高并发下Redis热key分治原理、亿级用户排行榜的范围分片设计、Redis Cluster集群持久化方式等内容。文章最后提供了大量面试真题和解决方案,帮助读者提升技术实力,顺利通过面试。
|
4月前
|
SQL 关系型数据库 MySQL
阿里面试:1000万级大表, 如何 加索引?
45岁老架构师尼恩在其读者交流群中分享了如何在生产环境中给大表加索引的方法。文章详细介绍了两种索引构建方式:在线模式(Online DDL)和离线模式(Offline DDL),并深入探讨了 MySQL 5.6.7 之前的“影子策略”和 pt-online-schema-change 方案,以及 MySQL 5.6.7 之后的内部 Online DDL 特性。通过这些方法,可以有效地减少 DDL 操作对业务的影响,确保数据的一致性和完整性。尼恩还提供了大量面试题和解决方案,帮助读者在面试中充分展示技术实力。
|
5月前
|
消息中间件 存储 canal
阿里面试:canal+MQ,会有乱序的问题吗?
本文详细探讨了在阿里面试中常见的问题——“canal+MQ,会有乱序的问题吗?”以及如何保证RocketMQ消息有序。文章首先介绍了消息有序的基本概念,包括全局有序和局部有序,并分析了RocketMQ中实现消息有序的方法。接着,针对canal+MQ的场景,讨论了如何通过配置`canal.mq.partitionsNum`和`canal.mq.partitionHash`来保证数据同步的有序性。最后,提供了多个与MQ相关的面试题及解决方案,帮助读者更好地准备面试,提升技术水平。
阿里面试:canal+MQ,会有乱序的问题吗?
|
5月前
|
消息中间件 架构师 Java
阿里面试:秒杀的分布式事务, 是如何设计的?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试阿里、滴滴、极兔等一线互联网企业时,遇到了许多关于分布式事务的重要面试题。为了帮助大家更好地应对这些面试题,尼恩进行了系统化的梳理,详细介绍了Seata和RocketMQ事务消息的结合,以及如何实现强弱结合型事务。文章还提供了分布式事务的标准面试答案,并推荐了《尼恩Java面试宝典PDF》等资源,帮助大家在面试中脱颖而出。