限界上下文之后:微服务怎么拆、上下文怎么聊?

上下文画好了,然后呢?------怎么部署、怎么通信,是紧接着的两个问题。

前一篇,我们花了一整篇文章讲限界上下文怎么划分:用事件风暴画边界,把"商品"在订单上下文、商品上下文、库存上下文里分别建模。

但画完边界之后,两个问题会立刻冒出来:

第一个问题:这些上下文,在部署上怎么落地?

订单上下文、商品上下文、库存上下文------它们是应该分别部署成三个独立的微服务,还是放在同一个微服务里?如果合并,合并到什么程度?如果拆分,拆到什么粒度?

第二个问题:上下文之间,怎么通信?

订单上下文需要知道库存够不够,库存上下文需要知道订单有没有支付。如果它们真的拆成了独立的微服务,怎么互相调用?用同步API?用异步消息?什么时候用哪个?

这两个问题不解决,限界上下文就只是一张画在PPT上的图------好看的摆设。

这篇文章把这两个问题一起讲。

读完这篇文章,你将获得:

✅ 理解微服务 ≠ 限界上下文 ------什么情况下1个上下文拆成多个服务,什么情况下多个上下文合并成1个服务

✅ 掌握拆与合的决策模型 ------从业务一致性、发布频率、技术异构三个维度判断

✅ 分清领域事件 vs 应用事件 vs 系统事件 ------它们各解决什么问题

✅ 学会同步API vs 异步事件 的选择标准------什么时候该用哪个

✅ 看清从战略设计到代码落地的完整路径------上下文画好了,代码怎么组织

一、先回顾:限界上下文画完之后的样子

前一篇,我们用一个电商系统做了事件风暴,划分出了五个限界上下文:

text 复制代码
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  订单上下文  │     │  商品上下文  │     │  库存上下文  │
│ (Order)     │     │ (Product)   │     │ (Inventory) │
└─────────────┘     └─────────────┘     └─────────────┘
┌─────────────┐     ┌─────────────┐
│  支付上下文  │     │  用户上下文  │
│ (Payment)   │     │ (User)      │
└─────────────┘     └─────────────┘

每个上下文内部,都有自己独立的领域模型:

java 复制代码
// 文件: order/domain/order/Order.java(订单上下文)
public class Order {
    private Long id;
    private Long userId;
    private List<OrderItem> items;  // 商品快照
    private OrderStatus status;
    // 订单自己的行为......
}

// 文件: inventory/domain/inventory/InventoryItem.java(库存上下文)
public class InventoryItem {
    private Long productId;
    private Integer availableStock;
    private Integer lockedStock;
    // 库存自己的行为......
}

// 文件: product/domain/product/Product.java(商品上下文)
public class Product {
    private Long id;
    private String name;
    private Category category;
    // 商品自己的行为......
}

现在问题来了:这些上下文在代码里怎么组织?在部署上怎么落地?

二、问题一:拆成微服务,还是合在一个服务里?

2.1 一个最常见的误解

很多人以为:一个限界上下文 = 一个微服务。

这句话对,也不对。

对的是:如果两个上下文业务边界清晰、团队边界清晰、数据独立性高,拆成两个微服务是合理的。

不对的是:如果机械地执行这个公式,你会得到灾难性的结果。

比如,你有一个"权限上下文"和"用户上下文",它们高度耦合------用户删除了,权限也要跟着删;用户角色变了,权限也要跟着变。如果强行拆成两个微服务,每次用户变更你都要跨服务调用,网络延迟、分布式事务、最终一致性问题全来了。

系统崩溃了,不是因为"拆错了",而是因为"不该拆的拆了"。

2.2 拆与合的决策模型

拆不拆,不看"是不是不同的上下文",而看三个维度。

维度一:业务一致性要求

如果两个上下文之间存在强一致性要求------A改了,B必须在同一个事务里立即改------它们应该合在同一个服务里,使用同一个数据库事务。

如果允许最终一致性------A改了,B可以几秒钟之后再改------它们可以拆成两个服务,通过消息异步同步。

判断标准:问业务方,"订单支付成功后,库存扣减可以延迟3秒吗?"如果回答"可以",拆;如果回答"绝对不行",合。

维度二:发布频率和变更节奏

如果两个上下文经常需要同时上线------改了订单逻辑必须同时改库存逻辑------它们应该合在一起。

如果它们各自独立发布------订单团队每周发两次,商品团队每月发一次------它们应该拆开。

判断标准:问两个团队的负责人,"你们能接受对方的发布节奏吗?"如果不能,拆。

维度三:技术异构

如果两个上下文需要不同的技术栈------一个用Java,一个用Go;一个用MySQL,一个用MongoDB------它们必须拆成独立的服务。

如果技术栈相同,这条不构成拆分的强制理由。

2.3 一张表帮你做决策

维度 合(1个服务) 拆(2个服务) 判断依据
一致性要求 强一致,必须同一个事务 最终一致,允许延迟 问业务方"能等几秒吗?"
发布频率 一起上线,节奏相同 各自独立,节奏不同 问团队"能接受对方节奏吗?"
技术栈 相同 不同 看代码仓库

2.4 三种映射模式

根据上述维度的综合判断,限界上下文到微服务有三种映射关系:

映射模式 含义 适用场景
1:1 一个上下文 = 一个微服务 理想情况,边界清晰,团队独立
1:N 一个上下文拆成多个微服务 上下文内部有子域,某些子域有特殊要求
N:1 多个上下文合并在一个微服务里 强一致性要求,或变更节奏相同

重要结论:限界上下文是业务边界,微服务是部署边界。业务边界不一定要等于部署边界------这是战略设计到技术落地的关键一步。

2.5 合与拆的示例

合的示例:权限上下文 + 用户上下文

用户删除时,权限必须同时删除------强一致性要求。如果拆成两个微服务,需要分布式事务,复杂且容易出错。所以,合在同一个微服务里

拆的示例:订单上下文中的"订单导出"子功能

订单导出是一个独立的子域,它和订单主流程的关系是:订单数据产生后,导出功能读取数据生成报表。它对一致性要求低(延迟几分钟甚至几小时都可以),并发量低,技术栈可以和主流程不同(用Go写导出服务更高效)。所以,拆成一个独立的微服务

代码体现

java 复制代码
// 方案:合------用户上下文和权限上下文在同一个服务里
// 文件: user/domain/user/User.java
public class User {
    // ... 用户字段
    
    public void delete() {
        this.status = UserStatus.DELETED;
        // 同一个事务里删除权限
        permissionRepository.deleteByUserId(this.id);
    }
}

// 方案:拆------订单导出作为独立服务
// 文件: order-export/infrastructure/event/OrderExportListener.java
@Component
public class OrderExportListener {
    @EventListener
    public void onOrderPaid(OrderPaidEvent event) {
        // 异步监听订单支付事件,生成导出数据
        // 不影响主订单流程
    }
}

三、问题二:上下文之间怎么通信?

拆完了,下一个问题来了:上下文的代码虽然分开了,但业务上它们必须协同工作。怎么协同?

你是一个商品上下文,订单上下文要查你的商品信息------怎么查?

你是一个订单上下文,库存上下文要扣库存------怎么扣?

有两种方式。

3.1 方式一:同步API调用

(关于openfeign的内容可以看我的另一篇文章深入解析 OpenFeign:从重试、拦截到负载均衡

订单上下文发起HTTP/RPC请求,等待商品上下文返回商品信息。请求-响应模式,阻塞等待。

java 复制代码
// 文件: order/infrastructure/acl/ProductApiClient.java
@Component
public class ProductApiClient {
    @Autowired private ProductApiFeignClient feignClient;
    
    public ProductDto getProduct(Long productId) {
        // 同步调用商品上下文的开放主机服务
        // 阻塞等待,直到商品上下文返回结果
        return feignClient.getProduct(productId);
    }
}

什么时候用:需要实时获取对方的当前状态。比如"下单时查商品价格",必须同步。

什么时候不用:对方响应慢,或者不需要实时结果。

3.2 方式二:异步事件通信

订单上下文发出一个"订单已支付"事件,库存上下文监听到这个事件后,自行扣减库存。发送即忘,非阻塞。

java 复制代码
// 文件: order/domain/event/OrderPaidEvent.java
public class OrderPaidEvent {
    private Long orderId;
    private Long userId;
    private List<OrderItem> items;
    private LocalDateTime paidAt;
}

// 文件: order/application/service/PayOrderAppService.java(发布事件)
@Service
public class PayOrderAppService {
    @Autowired private DomainEventPublisher eventPublisher;
    
    @Transactional
    public void handle(Long orderId) {
        // 1. 业务逻辑
        order.pay();
        orderRepo.save(order);
        
        // 2. 发布领域事件
        eventPublisher.publish(new OrderPaidEvent(orderId, userId, items));
    }
}

// 文件: inventory/application/listener/OrderPaidListener.java(订阅事件)
@Component
public class OrderPaidListener {
    @EventListener
    @Transactional
    public void onOrderPaid(OrderPaidEvent event) {
        // 库存上下文独立处理------扣减库存
        for (OrderItem item : event.getItems()) {
            inventoryService.deductStock(item.getProductId(), item.getQuantity());
        }
    }
}

什么时候用:不需要实时结果,允许最终一致性。比如"支付成功→扣库存→加积分"------扣库存可以等几秒,加积分可以等更久。

什么时候不用:需要对方的返回值来做业务决策。比如"下单时查商品价格",必须同步。

3.3 一张表看懂同步 vs 异步

对比维度 同步API 异步事件
通信方式 请求-响应 发布-订阅
阻塞性 阻塞等待 非阻塞
耦合度 高(依赖对方可用) 低(对方挂了不影响我)
一致性 强一致 最终一致
适用场景 查价格、查库存、查用户信息 支付后扣库存、积分增加、发通知
是否跨服务 通常跨服务 通常跨服务

经验法则:需要"问"对方时用同步,需要"通知"对方时用异步。

3.4 两种通信方式的代码组织对比

回到前一篇的"下单支付"场景,现在有两个上下文------订单上下文和库存上下文,我们看看两种通信方式的区别:

同步方式(API调用)

java 复制代码
// 文件: order/application/service/PayOrderAppService.java
@Service
public class PayOrderAppService {
    private final OrderRepository orderRepo;
    private final InventoryApiClient inventoryClient;  // 调用库存API
    
    @Transactional
    public void handle(Long orderId) {
        Order order = orderRepo.findById(orderId);
        order.pay();
        orderRepo.save(order);
        
        // 同步调用库存上下文扣库存
        // 如果库存扣减失败,订单支付也会回滚
        inventoryClient.deductStock(order.getItems());
    }
}

异步方式(事件驱动)

java 复制代码
// 文件: order/application/service/PayOrderAppService.java
@Service
public class PayOrderAppService {
    private final OrderRepository orderRepo;
    private final DomainEventPublisher eventPublisher;
    
    @Transactional
    public void handle(Long orderId) {
        Order order = orderRepo.findById(orderId);
        order.pay();
        orderRepo.save(order);
        
        // 只发布事件,不关心谁处理、怎么处理
        eventPublisher.publish(new OrderPaidEvent(orderId, order.getItems()));
    }
}

// 文件: inventory/application/listener/OrderPaidListener.java
@Component
public class OrderPaidListener {
    @EventListener
    public void onOrderPaid(OrderPaidEvent event) {
        // 库存上下文独立处理
        inventoryService.deductStock(event.getItems());
    }
}

四、把两件事放在一起看:一个完整的落地示例

用"下单支付"这个业务场景,把拆/合决策和通信方式放在一起看。

第一步:两个上下文怎么部署?

订单上下文和库存上下文------是拆成两个微服务,还是合在一个微服务里?

  • 业务一致性:支付成功→扣库存,可以接受3秒延迟(最终一致)
  • 发布频率:订单团队每天发布,库存团队每周发布,节奏不同
  • 技术异构:都用Java,技术栈相同

结论:拆成两个微服务(一致性允许最终一致 + 发布节奏不同)。

第二步:两个上下文怎么通信?

拆成两个微服务之后,订单上下文怎么通知库存上下文扣库存?

  • 订单不需要库存的返回值(扣没扣成不影响订单支付状态)
  • 允许最终一致(扣库存可以延迟几秒)

结论:用异步事件------订单发出"订单已支付"事件,库存订阅并处理。

完整代码

java 复制代码
// === 订单上下文(微服务A) ===

// 文件: order/domain/event/OrderPaidEvent.java
public class OrderPaidEvent {
    private Long orderId;
    private List<OrderItemSnapshot> items;
    private LocalDateTime paidAt;
}

// 文件: order/application/service/PayOrderAppService.java
@Service
public class PayOrderAppService {
    private final OrderRepository orderRepo;
    private final DomainEventPublisher eventPublisher;
    
    @Transactional
    public void handle(Long orderId) {
        Order order = orderRepo.findById(orderId);
        order.pay();
        orderRepo.save(order);
        // 发布事件------库存上下文会监听
        eventPublisher.publish(new OrderPaidEvent(orderId, order.getItems()));
    }
}

// === 库存上下文(微服务B) ===

// 文件: inventory/application/listener/OrderPaidListener.java
@Component
public class OrderPaidListener {
    private final InventoryService inventoryService;
    
    @EventListener
    public void onOrderPaid(OrderPaidEvent event) {
        // 扣减库存------自己的独立事务
        for (OrderItemSnapshot item : event.getItems()) {
            inventoryService.deductStock(item.getProductId(), item.getQuantity());
        }
    }
}

关键点

  • 订单上下文不知道库存上下文的存在------它只管发事件
  • 库存上下文不知道谁发了事件------它只管处理
  • 两者通过事件解耦,各自独立演进

五、领域事件、应用事件、系统事件:别搞混了

在异步事件通信中,很多人把三种不同的事件混为一谈。

事件类型 定义 例子 谁来发 谁来订阅
领域事件 领域内发生了业务上重要的事 OrderPaidEvent(订单已支付) 领域层 本上下文的其他聚合,或其他上下文
应用事件 应用层的流程状态变化 OrderPaymentStarted(支付流程开始) 应用层 同一个服务的其他应用组件
系统事件 技术层面的状态变化 CacheRefreshedEvent(缓存已刷新) 基础设施层 基础设施层

最容易混淆的是领域事件和应用事件。

区分标准很简单:问自己,这个事件业务方(非技术人员)听得懂吗?

  • 业务方听得懂"订单已支付" → 领域事件
  • 业务方听不懂"缓存已刷新" → 系统事件
  • 业务方可能听得懂"支付流程开始",但不关心 → 应用事件

核心结论 :上下文之间通信,只用领域事件。应用事件和系统事件都在上下文内部使用,不跨上下文。

六、战略设计 → 战术设计:完整的主脉

到现在为止,四篇文章已经串起了一条完整的路径:

阶段 做什么 产出
战略设计 事件风暴 → 划分限界上下文 上下文的边界和名称
战略→技术 上下文映射 → 确定协作关系 防腐层/开放主机服务的定义
战略→技术 拆/合决策 → 确定部署边界 多少个微服务,每服务包含哪些上下文
战略→技术 通信方式决策 → 确定交互方式 哪些用同步API,哪些用异步事件
战术设计 在每个上下文内部做战术设计 实体、值对象、聚合、仓储......
代码落地 按包结构组织代码 可运行的代码

把这一篇放到整个系列里,主脉变得更完整了:

三层架构有一个Service黑盒 → DDD把它拆成四层 → 类爆炸了,砍掉一半 → 系统大了,用限界上下文切分 → 切完了,决定怎么部署(微服务) → 部署完了,决定怎么通信(事件/API) → 最后在每个上下文内部做战术设计。

七、核心总结:一张表讲透"部署 + 通信"

如果你没时间读完全文,读完这张表就够了。

拆/合决策表

决策维度 合(1个微服务) 拆(2个微服务)
一致性 强一致 最终一致
发布频率 相同 不同
技术栈 相同 不同
代码体现 同一个模块,共享事务 不同服务,用事件同步

通信方式决策表

决策维度 同步API 异步事件
需要返回值 需要 不需要
实时性 强(立即) 弱(可延迟)
耦合度
适用场景 查询、校验 通知、触发
代码体现 HTTP/RPC调用 事件发布+订阅

写在最后

微服务的流行,让很多人忘了问"为什么要拆"。

"我们用了微服务"成了目的本身,而不是手段。结果是:一个简单的系统被拆成了十几个服务,运维复杂度暴涨,分布式事务满天飞,问题定位像大海捞针。

"能拆"和"该拆"是两件事。

如果你遵循了限界上下文的划分,但业务一致性要求很强------强行拆开只会带来灾难。合在一个服务里不是耻辱,是不该拆的时候不拆的智慧。

所以,回答开头的那个问题: "怎么判断该不该拆?"

该拆的时候: 两个上下文之间可以接受最终一致,且发布节奏不同,或技术栈不同。

不该拆的时候: 两个上下文之间必须强一致,改了A必须立即改B,或者团队规模小于等于5人。

微服务是你的架构手段,不是你的KPI。拆之前先想清楚:你是为了解决团队协作问题而拆,还是为了赶时髦而拆?

相关推荐
星火10241 小时前
【Groovy翻译-进阶篇】运行时元编程与编译时元编程(上)
后端·groovy
CHHH_HHH1 小时前
【Linux系统篇】深入解析Linux文件系统:从磁盘寻址到软硬链接
linux·服务器·开发语言·后端·ubuntu
星火10241 小时前
【Groovy翻译-进阶篇】运行时元编程与编译时元编程(下)
后端·groovy
用户106911794101 小时前
SpringSercuirty整合jwt
后端
yngsqq1 小时前
窗体快速取消
java·开发语言
岁月如歌77861 小时前
一次讲透 Redis 缓存击穿、穿透、雪崩,从原理到实战解决方案
java·后端·架构
IT_陈寒2 小时前
Vue的v-for为啥把我的渲染顺序搞乱套了?
前端·人工智能·后端
用户106911794102 小时前
SpringSecuirty自定义接口或者资源权限
后端
Python私教2 小时前
四个入口,一条流水线:如意智影的多入口架构取舍
人工智能·python·架构