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

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

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

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

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

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

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

订单上下文需要知道库存够不够,库存上下文需要知道订单有没有支付。如果它们真的拆成了独立的微服务,怎么互相调用?用同步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。拆之前先想清楚:你是为了解决团队协作问题而拆,还是为了赶时髦而拆?

相关推荐
打工仔折腾 AI7 小时前
工业场景下时序库与实时计算一体化架构选型实践对比
java·开发语言·后端·python·性能优化·架构·ai agent 实战
ZealSinger7 小时前
Java虚拟线程上线后瓶颈转移到哪看三层
java·开发语言·firefox
2601_961133287 小时前
盘立方软件指标文华期货公式下载
java
东方佑7 小时前
事件发生与智能:微观一致性与宏观涌现性
人工智能·深度学习·自然语言处理·架构·gru
蜗牛互联网7 小时前
长任务多Agent共享文件系统的Manifest交接模式
java·人工智能·后端
欣欣之王来了7 小时前
Python入门:什么是Python以及为什么选择它
学习·架构·面向对象·项目·python教程
Sirens.7 小时前
Java并发锁详解:六类锁策略与 synchronized 底层原理
java·前端·算法
代码方舟8 小时前
零信任架构实战:基于天远运营商三要素简版V即时版查询构建自动化电子投保实名核验网关
大数据·人工智能·架构·自动化
sunshine22 girl8 小时前
Java学习五 面向对象高级5 内部类1
java·学习
苏supper9 小时前
Spring Boot多环境配置与打包运行
spring boot·后端·spring