一、从一个真实问题说起
想象一下你正在负责一个电商平台,下单之后订单信息要同步到库存系统、支付系统、物流系统。听起来很简单对吧?但现实往往是这样的:订单这边显示"已支付",库存那边却还在扣减,物流那边还没收到发货指令,三个系统的数据永远对不上。
这种场景在实际开发中非常常见。问题的根源在于:不同的业务模块(我们后面会详细讲,这里先叫它"上下文")之间的数据格式、业务流程、状态定义都不一样,直接对接就容易出现数据不一致的问题。
领域驱动设计(DDD)里有一个概念叫"限界上下文映射",专门用来解决跨上下文交互时的数据一致性难题。说白了,就是当多个业务模块需要协作时,怎么让它们之间"说同一种语言",怎么保证数据不会在传递过程中走样。
这篇文章会用大白话讲清楚限界上下文映射的核心思想,再通过完整的代码示例带你动手实践。文章统一使用 Java 17 + Spring Boot 3.x + Spring Cloud 技术栈,所有代码都是可以直接运行参考的。
二、先搞明白:限界上下文到底是什么
2.1 用大白话理解限界上下文
限界上下文(Bounded Context)说白了就是一个"业务边界"。你可以把它理解成一家公司里的不同部门。财务部有自己的账本和记账规则,销售部有自己的客户管理和销售策略,IT部有自己的一套系统和工单流程。每个部门都有自己的语言体系,同一个词在不同部门可能有完全不同的含义。
比如说"用户"这个词:在用户中心系统里,用户就是一张身份证、一个手机号、一组登录凭证;在订单系统里,用户就是一个收货地址、一组消费记录、一个会员等级;在客服系统里,用户就是一个工单编号、一组投诉记录。虽然都叫"用户",但每个上下文对"用户"的理解是不一样的。
限界上下文的核心思想就是:承认这些差异的存在,明确划定每个上下文的边界,在边界之内,同一个概念只用一种方式去理解和处理。
2.2 限界上下文的边界在哪里
边界怎么画?这取决于业务需求。一个简单的判断标准是:如果两个业务功能经常一起变化、需要保持一致性,那它们大概率在同一个上下文里;如果两个业务功能变化节奏完全不同、数据格式差异很大,那它们应该在不同上下文里。
拿电商来说,"下单"和"支付"变化节奏完全不同。下单可能一天改十次需求(加个优惠券、加个备注),支付可能一年改一次(对接新的支付渠道)。所以下单和支付应该在不同的限界上下文里,然后通过映射机制来协调。
/**
* 技术栈:Java 17 + Spring Boot 3.x
* 说明:下面这个例子展示了为什么同一份"用户"数据在不同的上下文里长得不一样
*/
// ===== 用户中心上下文的"用户"定义 =====
@Data
public class UserCenterProfile {
private String userId; // 用户唯一标识
private String realName; // 真实姓名
private String phone; // 手机号
private String idCard; // 身份证号
private LocalDateTime createdAt; // 注册时间
}
// ===== 订单上下文的"用户"定义 =====
@Data
public class OrderCustomer {
private String orderId; // 订单ID
private String customerName; // 收货人姓名
private String deliveryPhone; // 收货电话
private String address; // 收货地址
private Integer memberLevel; // 会员等级
}
// ===== 客服上下文的"用户"定义 =====
@Data
public class ServiceUser {
private String ticketId; // 工单ID
private String contactPhone; // 联系电话
private String complaintReason; // 投诉原因
private LocalDateTime contactTime; // 联系时间
}
/*
* 看清楚了吗?
* 三个上下文里都有"用户"这个概念,
* 但是每个上下文关注的数据字段完全不同:
* - 用户中心关心的是身份认证相关字段
* - 订单上下文关心的是收货和消费相关字段
* - 客服上下文关心的是沟通和服务相关字段
*
* 如果把用户中心的数据直接丢给订单系统用,那订单系统会发现:
* - 缺了address字段
* - 缺了memberLevel字段
* - 多了一堆它根本不关心的idCard、createdAt
*
* 这就是跨上下文直接交互会导致不一致的原因。
*/
三、限界上下文映射的核心思想
3.1 映射的本质
限界上下文映射(Context Map)就是一张"关系图",描述了不同限界上下文之间是怎么协作的。这张图不是为了好看,而是为了解决一个核心问题:当两个上下文需要交互时,用什么方式保证数据的一致性?
DDD里定义了好几种映射模式,每种模式对应不同的协作关系和一致性策略。下面我会挑最常用的几种,逐一讲清楚,并且给出完整的代码实现。
3.2 先认识一下参与者
在讲具体的映射模式之前,先认识两个角色:
- 上游(Upstream):提供数据和服务的那一方。比如用户中心就是上游,它提供用户数据给其他人用。
- 下游(Downstream):接收数据和服务的那一方。比如订单系统就是下游,它从用户中心拿用户数据。
下游通常处于"弱势"地位——上游怎么给数据,下游就怎么用,因为下游没有权利要求上游改。这个"弱势"的关系是很多不一致问题的根源,也是我们需要映射模式来保护的关键点。
四、上下游模式:最简单的协作方式
4.1 上下游模式(Upstream/Downstream)
上下游模式就是最朴素的协作方式:上游提供数据,下游直接使用,不做任何转换。这种模式的好处是简单直接,坏处是一旦上游改了数据格式,下游就会出乱子。
什么时候用这种模式?当上游和下游的变更节奏基本同步,而且下游有能力跟上上游的变化时。比如你团队内部的一个小系统,用户中心和订单系统都在同一个团队维护,上游改数据的同时就会通知下游一起改,这时候上下游模式就够了。
4.2 代码实现
/**
* 技术栈:Java 17 + Spring Boot 3.x
* 场景:上下游模式 - 订单系统直接使用用户中心的数据
*
* 适用条件:上下游变更节奏同步,团队有协同能力
* 风险:上游一变,下游就崩
*/
@Service
public class OrderServiceDirectImpl {
// 注入用户中心的客户端(通常是Feign Client或RestTemplate)
@Autowired
private UserCenterClient userCenterClient;
/**
* 创建订单 - 直接从上游获取用户数据
*
* @param userId 用户ID
* @param productId 商品ID
* @return 创建的订单
*/
public Order createOrder(String userId, String productId) {
// 第一步:从上游获取用户信息
// 注意:这里直接使用上游的数据结构,没有做任何转换
UserCenterProfile userProfile = userCenterClient.getUserById(userId);
// 第二步:直接用上游数据构建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setStatus("CREATED");
// 这里直接用上游的字段,上游如果改了字段名,这里就编译不过
order.setCustomerName(userProfile.getRealName()); // 依赖上游的realName字段
order.setDeliveryPhone(userProfile.getPhone()); // 依赖上游的phone字段
// 第三步:保存订单
orderRepository.save(order);
return order;
}
}
/**
* 这个Feign Client直接返回上游的数据结构
* 这意味着订单系统的代码里直接引用了用户中心的DTO
*/
@FeignClient(name = "user-center-service", url = "http://user-center-service:8080")
public interface UserCenterClient {
@GetMapping("/api/users/{userId}")
UserCenterProfile getUserById(@PathVariable("userId") String userId);
}
/*
* 问题出在哪里?
*
* 1. 耦合问题:订单系统直接依赖UserCenterProfile这个类。
* 如果用户中心把realName改成fullName,订单系统就要跟着改。
*
* 2. 字段不匹配问题:订单系统需要address和memberLevel,
* 但UserCenterProfile里没有这两个字段。
* 订单系统要么自己补充数据,要么跟用户中心扯皮要求加字段。
*
* 3. 版本同步问题:用户中心发布了新版本(v2),改了数据结构,
* 但订单系统还没升级,这时候两边就不一致了。
*
* 上下游模式适合"信任度高、变更同步"的内部场景。
* 如果是跨团队、跨部门的协作,强烈建议不要用这个模式。
*/
五、客户供应商模式:有商量的合作
5.1 客户供应商模式(Customer-Supplier)
客户供应商模式比上下游模式多了一层"协商"。下游(客户)可以向上游(供应商)提出需求,要求上游按照下游需要的方式提供数据。这就像你和供应商说:"我要的螺丝规格是这样,你按这个给我做。"
这个模式的前提是:下游对客户有足够的"话语权"。如果下游业务很重要,上游很依赖这个客户,那上游大概率会同意下游的提议。如果下游是小弟,上游是大佬,那这个模式就不适用了。
5.2 代码实现
/**
* 技术栈:Java 17 + Spring Boot 3.x
* 场景:客户供应商模式 - 订单系统向用户中心提出数据需求
*
* 核心思想:下游主动定义自己需要的数据格式,上游按照下游的需求提供
* 适用条件:下游有话语权,上下游有协商机制
*/
/**
* 订单系统定义自己需要的用户数据结构
* 注意:这个类是订单系统自己定义的,不依赖用户中心的DTO
*/
@Data
public class OrderRequiredUserInfo {
/** 用户ID - 必填 */
@NotBlank(message = "用户ID不能为空")
private String userId;
/** 收货人姓名 - 必填 */
@NotBlank(message = "收货人姓名不能为空")
private String customerName;
/** 收货电话 - 必填 */
@NotBlank(message = "收货电话不能为空")
private String deliveryPhone;
/** 收货地址 - 必填 */
@NotBlank(message = "收货地址不能为空")
private String address;
/** 会员等级 - 可选,默认为0 */
private Integer memberLevel = 0;
}
/**
* 用户中心按照订单系统的需求提供数据
* 用户中心暴露一个专门的接口,返回订单系统需要的格式
*/
@RestController
@RequestMapping("/api/v1/suppliers/order-service")
public class UserCenterForOrderController {
@Autowired
private UserProfileRepository profileRepository;
@Autowired
private MemberService memberService;
/**
* 专门为订单系统提供的用户信息接口
*
* @param userId 用户ID
* @return 订单系统需要的用户数据格式
*/
@GetMapping("/user/{userId}")
public ResponseEntity<OrderRequiredUserInfo> getUserForOrderService(
@PathVariable String userId) {
// 从用户中心查出用户信息
UserProfile userProfile = profileRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId));
// 组装订单系统需要的数据
OrderRequiredUserInfo info = new OrderRequiredUserInfo();
info.setUserId(userProfile.getUserId());
info.setCustomerName(userProfile.getRealName());
info.setDeliveryPhone(userProfile.getPhone());
info.setAddress(userProfile.getDefaultAddress());
// 调用会员服务获取会员等级
Integer memberLevel = memberService.getMemberLevel(userId);
info.setMemberLevel(memberLevel);
return ResponseEntity.ok(info);
}
}
/**
* 订单系统的调用方 - 使用自己的数据结构
*/
@Service
public class OrderServiceWithCustomerSupplier {
@Autowired
private UserCenterClientForOrder userCenterClient;
public Order createOrder(String userId, String productId) {
// 获取用户信息 - 使用的是订单系统自己定义的格式
OrderRequiredUserInfo userInfo = userCenterClient.getUserForOrder(userId);
// 校验数据完整性
validateUserInfo(userInfo);
// 构建订单
Order order = new Order();
order.setUserId(userInfo.getUserId());
order.setProductId(productId);
order.setCustomerName(userInfo.getCustomerName());
order.setDeliveryPhone(userInfo.getDeliveryPhone());
order.setAddress(userInfo.getAddress());
order.setMemberLevel(userInfo.getMemberLevel());
order.setStatus("CREATED");
return orderRepository.save(order);
}
/**
* 数据校验 - 确保上游传来的数据符合下游的需要
*/
private void validateUserInfo(OrderRequiredUserInfo info) {
if (info == null) {
throw new BusinessException("用户信息为空");
}
if (info.getMemberLevel() == null || info.getMemberLevel() < 0) {
throw new BusinessException("会员等级无效");
}
}
}
@FeignClient(
name = "user-center-service",
url = "http://user-center-service:8080",
path = "/api/v1/suppliers/order-service"
)
public interface UserCenterClientForOrder {
@GetMapping("/user/{userId}")
OrderRequiredUserInfo getUserForOrder(@PathVariable("userId") String userId);
}
/*
* 客户供应商模式的优点:
* 1. 下游定义自己的数据结构,不受上游内部结构变化影响
* 2. 数据格式由下游说了算,确保数据一致性
* 3. 上游专门为下游暴露接口,职责清晰
*
* 缺点:
* 1. 需要上下游协商,有沟通成本
* 2. 上游要维护多个版本的接口(不同下游可能要不同格式)
* 3. 如果下游很多,上游会被各种"定制需求"搞得很痛苦
*/
六、防腐层模式:最实用的隔离方案
6.1 防腐层模式(Anti-Corruption Layer)
防腐层模式是我在实际项目中用得最多的一个。它的核心思想是:下游不直接使用上游的数据,而是在中间加一层"翻译层",把上游的数据转换成下游需要的格式。
这就像你出国旅行,当地人和你语言不通,你需要一个翻译。翻译把当地人的话翻译成你能听懂的话,你再把意思表达给翻译,翻译再转述给当地人。翻译层就是"防腐层",它的作用是防止上游的"脏数据"(格式不一致、业务逻辑不同)污染下游。
6.2 代码实现
/**
* 技术栈:Java 17 + Spring Boot 3.x
* 场景:防腐层模式 - 订单系统通过防腐层隔离用户中心的数据
*
* 核心思想:在下游和上游之间加一层翻译,隔离两边的数据结构差异
* 适用条件:跨团队、跨部门协作,上游不受自己控制
*/
/**
* 防腐层 - 适配器
* 职责:把上游的数据格式转换成下游需要的格式
*/
@Component
public class UserCenterAntiCorruptionLayer {
private static final Logger log = LoggerFactory.getLogger(UserCenterAntiCorruptionLayer.class);
@Autowired
private UserCenterClient userCenterClient;
/**
* 从用户中心获取用户信息,并转换成订单系统需要的格式
*
* @param userId 用户ID
* @return 订单系统内部使用的用户数据
*/
public OrderCustomerAdapter fetchOrderCustomer(String userId) {
// 第一步:调用上游接口,获取上游格式的数据
UserCenterProfile upstreamProfile = userCenterClient.getUserById(userId);
// 第二步:做数据校验
if (upstreamProfile == null) {
log.warn("上游未返回用户信息, userId={}", userId);
throw new DataInconsistencyException("用户不存在: " + userId);
}
// 第三步:字段映射 - 把上游的字段转换成下游需要的字段
OrderCustomerAdapter adapter = new OrderCustomerAdapter();
adapter.setCustomerId(userId);
// 上游字段realName -> 下游字段customerName
adapter.setCustomerName(sanitizeName(upstreamProfile.getRealName()));
// 上游字段phone -> 下游字段deliveryPhone
adapter.setDeliveryPhone(normalizePhone(upstreamProfile.getPhone()));
// 上游没有address字段,需要从地址服务补充
String address = fetchAddressFromAddressService(userId);
adapter.setAddress(address);
// 上游没有memberLevel字段,需要从会员服务补充
Integer level = fetchMemberLevelFromMemberService(userId);
adapter.setMemberLevel(level);
// 记录转换日志,方便排查问题
log.info("防腐层转换完成, userId={}, 客户名={}, 电话={}",
userId, adapter.getCustomerName(), adapter.getDeliveryPhone());
return adapter;
}
/**
* 清洗姓名 - 处理上游可能传来的脏数据
*/
private String sanitizeName(String name) {
if (name == null || name.trim().isEmpty()) {
return "匿名用户"; // 上游没传姓名,给个默认值
}
// 去掉前后空格,去掉特殊字符
return name.trim().replaceAll("[^\\u4e00-\\u9fa5a-zA-Z]", "");
}
/**
* 标准化电话号码 - 确保格式统一
*/
private String normalizePhone(String phone) {
if (phone == null) {
throw new DataInconsistencyException("上游返回的电话号码为空");
}
// 去掉非数字字符
String digits = phone.replaceAll("[^0-9]", "");
// 如果是11位手机号,直接返回
if (digits.length() == 11) {
return digits;
}
// 如果是10位,补个0
if (digits.length() == 10) {
return "0" + digits;
}
// 长度不对,记录告警
log.warn("电话号码格式异常, phone={}, 处理后={}", phone, digits);
return digits;
}
/**
* 从地址服务获取地址 - 解决上游没有这个字段的问题
*/
private String fetchAddressFromAddressService(String userId) {
try {
AddressInfo address = addressServiceClient.getDefaultAddress(userId);
if (address == null) {
log.warn("用户未设置默认地址, userId={}", userId);
return "地址待完善";
}
return address.getProvince() + address.getCity()
+ address.getDistrict() + address.getDetail();
} catch (Exception e) {
log.error("获取地址失败, userId={}", userId, e);
return "地址待完善";
}
}
/**
* 从会员服务获取会员等级
*/
private Integer fetchMemberLevelFromMemberService(String userId) {
try {
return memberServiceClient.getMemberLevel(userId);
} catch (Exception e) {
log.error("获取会员等级失败, userId={}", userId, e);
return 0; // 默认普通会员
}
}
}
/**
* 防腐层的输出 - 订单系统内部使用的用户数据格式
*/
@Data
public class OrderCustomerAdapter {
/** 客户ID */
private String customerId;
/** 收货人姓名(已清洗) */
private String customerName;
/** 收货电话(已标准化) */
private String deliveryPhone;
/** 完整地址(已补全) */
private String address;
/** 会员等级(已补全) */
private Integer memberLevel;
}
/**
* 订单服务 - 使用防腐层的输出,完全不感知上游的数据结构
*/
@Service
public class OrderServiceWithAntiCorruptionLayer {
@Autowired
private UserCenterAntiCorruptionLayer antiCorruptionLayer;
@Autowired
private OrderRepository orderRepository;
/**
* 创建订单 - 通过防腐层获取用户数据
*/
public Order createOrder(String userId, String productId) {
// 调用防腐层获取已转换的用户数据
// 订单系统完全不需要知道上游的数据结构是什么
OrderCustomerAdapter customer = antiCorruptionLayer.fetchOrderCustomer(userId);
// 直接用转换后的数据构建订单
Order order = new Order();
order.setCustomerId(customer.getCustomerId());
order.setProductId(productId);
order.setCustomerName(customer.getCustomerName());
order.setDeliveryPhone(customer.getDeliveryPhone());
order.setAddress(customer.getAddress());
order.setMemberLevel(customer.getMemberLevel());
order.setStatus("CREATED");
return orderRepository.save(order);
}
}
/**
* 自定义异常 - 用于数据一致性问题
*/
public class DataInconsistencyException extends RuntimeException {
public DataInconsistencyException(String message) {
super(message);
}
public DataInconsistencyException(String message, Throwable cause) {
super(message, cause);
}
}
/*
* 防腐层模式的优点:
* 1. 完全隔离上下游,上游怎么变都不影响下游
* 2. 可以在转换层做数据校验、清洗、补全
* 3. 上游数据出问题时,下游可以优雅降级(比如返回默认值)
* 4. 出了问题容易排查,转换逻辑都在防腐层里
*
* 缺点:
* 1. 多了一层代码,开发成本稍高
* 2. 如果上游频繁变,防腐层也要跟着频繁改
* 3. 防腐层本身也可能成为新的维护负担
*
* 最佳实践建议:
* - 防腐层要记录详细的转换日志
* - 防腐层要做数据校验,不要把脏数据传下去
* - 对于关键字段,转换失败要抛异常;对于非关键字段,可以给默认值
*/
七、共享内核模式:共用一套核心逻辑
7.1 共享内核模式(Shared Kernel)
共享内核模式是指两个或多个限界上下文共用一部分核心逻辑或数据结构。说白了就是"大家都用同一套代码"。
这种模式听起来很美——共用一套代码,数据肯定一致啊。但实际操作中很容易出问题。比如两个上下文的人同时改了这个共享代码,谁改的谁说了算?一个人觉得这样改更合理,另一个人觉得那样改才对,最后代码就乱了。
共享内核模式适合的场景是:两个上下文的关系非常紧密,由同一个团队维护,而且共享的部分很少(只有核心的值对象或者领域服务)。
7.2 代码实现
/**
* 技术栈:Java 17 + Spring Boot 3.x
* 场景:共享内核模式 - 订单系统和支付系统共用"货币"领域模型
*
* 核心思想:多个上下文共用一个核心的领域模型
* 适用条件:同团队维护,共享部分少且稳定
* 注意:共享内核要严格控制,只放最核心的、最稳定的代码
*/
// ===== 共享内核模块(独立打成jar包) =====
/**
* 货币值对象 - 订单系统和支付系统共用
* 这个类放在shared-kernel模块里,独立打包发布
*/
public final class Money {
private final BigDecimal amount;
private final CurrencyType currencyType;
private Money(BigDecimal amount, CurrencyType currencyType) {
this.amount = amount;
this.currencyType = currencyType;
}
/**
* 工厂方法 - 创建人民币金额
*/
public static Money ofRMB(BigDecimal amount) {
validateAmount(amount);
return new Money(amount, CurrencyType.CNY);
}
/**
* 工厂方法 - 创建美元金额
*/
public static Money ofUSD(BigDecimal amount) {
validateAmount(amount);
return new Money(amount, CurrencyType.USD);
}
/**
* 加法运算 - 只允许相同币种相加
*/
public Money plus(Money other) {
if (this.currencyType != other.currencyType) {
throw new CurrencyMismatchException(
this.currencyType, other.currencyType);
}
return new Money(this.amount.add(other.amount), this.currencyType);
}
/**
* 减法运算
*/
public Money minus(Money other) {
if (this.currencyType != other.currencyType) {
throw new CurrencyMismatchException(
this.currencyType, other.currencyType);
}
return new Money(this.amount.subtract(other.amount), this.currencyType);
}
/**
* 乘法运算
*/
public Money multiply(BigDecimal factor) {
validateAmount(factor);
return new Money(this.amount.multiply(factor), this.currencyType);
}
/**
* 判断是否为零
*/
public boolean isZero() {
return amount.compareTo(BigDecimal.ZERO) == 0;
}
/**
* 金额不能为负数
*/
private static void validateAmount(BigDecimal amount) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
}
// Getters...
public BigDecimal getAmount() { return amount; }
public CurrencyType getCurrencyType() { return currencyType; }
}
/**
* 币种枚举
*/
public enum CurrencyType {
CNY("人民币"),
USD("美元"),
EUR("欧元");
private final String displayName;
CurrencyType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() { return displayName; }
}
/**
* 币种不匹配异常
*/
public class CurrencyMismatchException extends RuntimeException {
public CurrencyMismatchException(CurrencyType a, CurrencyType b) {
super(String.format("币种不匹配: %s vs %s", a, b));
}
}
// ===== 订单系统使用共享内核 =====
@Service
public class OrderServiceUsingSharedKernel {
/**
* 计算订单总价 - 使用共享内核的Money对象
*/
public OrderCalculateResult calculateOrderTotal(List<OrderItem> items) {
// 使用共享内核的Money类
Money totalAmount = Money.ofRMB(BigDecimal.ZERO);
for (OrderItem item : items) {
Money itemTotal = item.getUnitPrice()
.multiply(new BigDecimal(item.getQuantity()));
totalAmount = totalAmount.plus(itemTotal);
}
// 如果订单金额大于1000,打个9折
if (totalAmount.getAmount().compareTo(new BigDecimal("1000")) > 0) {
totalAmount = totalAmount.multiply(new BigDecimal("0.9"));
}
return new OrderCalculateResult(totalAmount);
}
}
// ===== 支付系统使用共享内核 =====
@Service
public class PaymentServiceUsingSharedKernel {
/**
* 处理支付 - 直接使用共享内核的Money对象
*/
public PaymentResult processPayment(String orderId, Money amount) {
// 直接使用共享内核的Money,不需要任何转换
if (amount.isZero()) {
throw new InvalidPaymentException("支付金额不能为零");
}
// 调用支付渠道
PaymentResponse response = paymentGatewayClient.pay(
orderId,
amount.getAmount(),
amount.getCurrencyType().name()
);
return new PaymentResult(response.getTransactionId(), amount);
}
}
/*
* 共享内核模式的优点:
* 1. 数据格式100%一致,不存在转换问题
* 2. 核心业务逻辑只写一次,避免重复实现
* 3. 对核心概念的修改只需要改一处
*
* 缺点(非常重要,务必注意):
* 1. 修改共享内核需要所有使用方一起升级,协调成本高
* 2. 一个上下文的bug可能影响所有其他上下文
* 3. 共享内核容易变成"垃圾桶",什么人都往里塞代码
*
* 最佳实践建议:
* - 共享内核只放值对象和领域服务,不放实体
* - 共享内核要独立版本号,严格管控变更
* - 每个上下文的业务逻辑不要放共享内核里
* - 共享内核的变更要经过所有使用方的评审
*/
八、开放主机服务模式:上游统一出口
8.1 开放主机服务模式(Open Host Service)
开放主机服务模式是指上游提供一个标准化的接口,多个下游都通过这个接口获取数据。上游负责维护接口的稳定性和数据的一致性。
这就像机场的航班信息显示屏——所有航空公司都往这个显示屏上放数据,所有旅客都从这个显示屏上看数据。显示屏的数据来源可能是很多个,但旅客只需要看一个出口。
8.2 代码实现
/**
* 技术栈:Java 17 + Spring Boot 3.x
* 场景:开放主机服务模式 - 商品中心为多个下游提供统一的商品数据
*
* 核心思想:上游提供一个标准化的"出口",所有下游都从这个出口获取数据
* 适用条件:上游有多个下游,且希望统一管理接口
*/
/**
* 开放主机服务 - 标准化的商品查询接口
* 这是商品中心对外暴露的统一出口
*/
@RestController
@RequestMapping("/api/v1/openhost")
public class ProductOpenHostController {
private static final Logger log = LoggerFactory.getLogger(ProductOpenHostController.class);
@Autowired
private ProductService productService;
@Autowired
private ProductCacheService productCacheService;
/**
* 查询商品详情 - 标准化接口
*
* @param productId 商品ID
* @param caller 调用方标识(用于日志和权限控制)
* @return 标准化的商品数据
*/
@GetMapping("/products/{productId}")
public ResponseEntity<ProductOpenApiResponse> getProduct(
@PathVariable String productId,
@RequestHeader("X-Caller-Id") String caller) {
log.info("开放接口被调用, caller={}, productId={}", caller, productId);
// 先查缓存
ProductOpenApiResponse cached = productCacheService.getFromCache(productId);
if (cached != null) {
return ResponseEntity.ok(cached);
}
// 缓存没有,查数据库
Product product = productService.getProductById(productId);
// 组装标准化的响应数据
ProductOpenApiResponse response = assembleStandardResponse(product);
// 写入缓存
productCacheService.putToCache(productId, response);
return ResponseEntity.ok(response);
}
/**
* 批量查询商品 - 支持多个下游一次查询多个商品
*/
@PostMapping("/products/batch")
public ResponseEntity<List<ProductOpenApiResponse>> getProductsBatch(
@RequestBody BatchProductRequest request,
@RequestHeader("X-Caller-Id") String caller) {
log.info("批量查询被调用, caller={}, 数量={}",
caller, request.getProductIds().size());
if (request.getProductIds().size() > 100) {
return ResponseEntity.badRequest().build(); // 限制单次最多100个
}
List<ProductOpenApiResponse> results = request.getProductIds().stream()
.map(id -> {
ProductOpenApiResponse cached = productCacheService.getFromCache(id);
if (cached != null) return cached;
Product p = productService.getProductById(id);
ProductOpenApiResponse r = assembleStandardResponse(p);
productCacheService.putToCache(id, r);
return r;
})
.toList();
return ResponseEntity.ok(results);
}
/**
* 组装标准化的商品数据
* 注意:这里把商品中心内部的各种数据格式统一转换成标准格式
*/
private ProductOpenApiResponse assembleStandardResponse(Product product) {
ProductOpenApiResponse response = new ProductOpenApiResponse();
response.setProductId(product.getProductId());
response.setProductName(product.getName());
response.setCategory(product.getCategory());
response.setDescription(product.getDescription());
// 统一价格格式:不管内部怎么存的,统一转成分
response.setPriceCents(product.getPrice().multiply(new BigDecimal("100"))
.longValue());
// 统一库存格式
response.setStockQuantity(product.getStock());
// 统一状态枚举
response.setStatus(product.getStatus().name());
return response;
}
}
/**
* 标准化的商品数据 - 所有下游都用这个格式
*/
@Data
public class ProductOpenApiResponse {
private String productId;
private String productName;
private String category;
private String description;
/** 价格,单位:分(避免浮点数精度问题) */
private Long priceCents;
private Integer stockQuantity;
private String status; // ON_SALE, OFF_SALE, SOLD_OUT
}
/**
* 批量查询请求
*/
@Data
public class BatchProductRequest {
private List<String> productIds;
}
/**
* 下游A:订单系统调用开放接口
*/
@Service
public class OrderServiceCallingOpenHost {
@Autowired
private RestTemplate restTemplate;
public Order createOrder(String userId, List<CartItem> cartItems) {
Order order = new Order();
order.setUserId(userId);
order.setStatus("CREATED");
List<OrderItem> orderItems = new ArrayList<>();
Long totalAmountCents = 0L;
for (CartItem item : cartItems) {
// 调用商品中心的开放接口获取商品信息
String url = String.format(
"http://product-center:8080/api/v1/openhost/products/%s",
item.getProductId()
);
HttpHeaders headers = new HttpHeaders();
headers.set("X-Caller-Id", "order-service");
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<ProductOpenApiResponse> response = restTemplate.exchange(
url,
HttpMethod.GET,
new HttpEntity<>(headers),
ProductOpenApiResponse.class
);
ProductOpenApiResponse product = response.getBody();
// 校验商品状态
if (!"ON_SALE".equals(product.getStatus())) {
throw new BusinessException("商品已下架: " + product.getProductName());
}
// 校验库存
if (product.getStockQuantity() < item.getQuantity()) {
throw new BusinessException("库存不足: " + product.getProductName());
}
// 计算金额(使用分作为单位,避免精度问题)
Long itemAmountCents = product.getPriceCents() * item.getQuantity();
totalAmountCents += itemAmountCents;
OrderItem orderItem = new OrderItem();
orderItem.setProductId(product.getProductId());
orderItem.setProductName(product.getProductName());
orderItem.setPriceCents(product.getPriceCents());
orderItem.setQuantity(item.getQuantity());
orderItem.setAmountCents(itemAmountCents);
orderItems.add(orderItem);
}
order.setItems(orderItems);
order.setTotalAmountCents(totalAmountCents);
return order;
}
}
/*
* 开放主机服务模式的优点:
* 1. 上游统一管控接口,保证数据一致性
* 2. 多个下游用同一套标准格式,降低对接成本
* 3. 上游可以统一管理缓存、限流、监控等横切关注点
*
* 缺点:
* 1. 上游需要维护多个版本的接口(向后兼容)
* 2. 上游的变更需要通知所有下游,协调成本高
* 3. 如果上游挂了,所有下游都受影响(单点依赖)
*
* 最佳实践建议:
* - 使用版本号区分接口版本(v1, v2)
* - 提供完善的接口文档和SDK
* - 做好缓存和降级策略
* - 使用X-Caller-Id之类的标识调用方,方便排查问题
*/
九、发布语言模式:用消息解耦
9.1 发布语言模式(Published Language)
发布语言模式是一种通过消息来解耦上下游的方式。上游发布一个标准格式的消息,下游订阅这个消息并根据需要做处理。上下游不需要实时通信,也不需要直接依赖对方的代码。
这就像广播电台——电台发布节目内容(消息),观众自己选择听不听、什么时候听(订阅和消费)。电台不需要知道观众是谁,观众也不需要知道电台内部怎么制作的。
发布语言模式的核心在于定义一个清晰的"发布语言"——消息的格式、字段、语义都要约定清楚。这种模式特别适合处理异步场景,比如下单之后发通知、扣库存、更新积分等。
9.2 代码实现
/**
* 技术栈:Java 17 + Spring Boot 3.x + Spring Cloud Stream (基于Kafka)
* 场景:发布语言模式 - 订单系统发布订单创建事件,多个下游异步消费
*
* 核心思想:上游发布标准化消息,下游异步订阅消费
* 适用条件:需要异步解耦的场景,下游处理不需要实时反馈
*/
// ===== 事件定义(发布语言) =====
/**
* 订单创建事件 - 标准化的消息格式
* 这个事件定义了所有下游都能理解的"发布语言"
*/
@Data
@Builder
public class OrderCreatedEvent {
/** 事件ID - 唯一标识一条消息 */
private String eventId;
/** 事件类型 - 固定值 */
private String eventType = "ORDER_CREATED";
/** 事件发生时间 */
private LocalDateTime occurredAt;
/** 事件版本号 - 用于处理消息格式变更 */
private String eventVersion = "1.0";
/** 订单ID */
private String orderId;
/** 用户ID */
private String userId;
/** 订单总金额,单位:分 */
private Long totalAmountCents;
/** 订单状态 */
private String orderStatus;
/** 订单明细 */
private List<OrderItemDTO> items;
/** 收货信息 */
private ShippingInfoDTO shippingInfo;
/** 支付方式 */
private String paymentMethod;
}
/**
* 订单明细DTO - 事件里携带的数据
*/
@Data
@Builder
public class OrderItemDTO {
private String productId;
private String productName;
private Long priceCents;
private Integer quantity;
private Long amountCents;
}
/**
* 收货信息DTO
*/
@Data
@Builder
public class ShippingInfoDTO {
private String customerName;
private String deliveryPhone;
private String address;
}
// ===== 上游:订单系统发布事件 =====
@Service
public class OrderEventPublisher {
private static final Logger log = LoggerFactory.getLogger(OrderEventPublisher.class);
// 注入消息发送器
// 使用Spring Cloud Stream,底层可以是Kafka/RabbitMQ等
@Autowired
private MessageChannel orderEventOutput;
/**
* 发布订单创建事件
*
* @param order 已创建的订单
*/
public void publishOrderCreated(Order order) {
// 构建事件
OrderCreatedEvent event = OrderCreatedEvent.builder()
.eventId(UUID.randomUUID().toString())
.orderId(order.getId())
.userId(order.getUserId())
.totalAmountCents(order.getTotalAmountCents())
.orderStatus(order.getStatus())
.occurredAt(LocalDateTime.now())
.paymentMethod(order.getPaymentMethod())
.items(order.getItems().stream()
.map(item -> OrderItemDTO.builder()
.productId(item.getProductId())
.productName(item.getProductName())
.priceCents(item.getPriceCents())
.quantity(item.getQuantity())
.amountCents(item.getAmountCents())
.build())
.toList())
.shippingInfo(ShippingInfoDTO.builder()
.customerName(order.getCustomerName())
.deliveryPhone(order.getDeliveryPhone())
.address(order.getAddress())
.build())
.build();
// 发送消息
Message<OrderCreatedEvent> message = MessageBuilder
.withPayload(event)
.setHeader("x-order-id", order.getId())
.setHeader("x-user-id", order.getUserId())
.setHeader("x-timestamp", System.currentTimeMillis())
.build();
boolean success = orderEventOutput.send(message);
if (success) {
log.info("订单创建事件发布成功, orderId={}, eventId={}",
order.getId(), event.getEventId());
} else {
log.error("订单创建事件发布失败, orderId={}", order.getId());
throw new MessagePublishException("订单事件发布失败");
}
}
}
// ===== 下游A:库存系统消费事件 =====
/**
* 库存消费者 - 接收订单创建事件,扣减库存
*/
@Service
public class InventoryOrderEventHandler {
private static final Logger log = LoggerFactory.getLogger(InventoryOrderEventHandler.class);
@Autowired
private InventoryService inventoryService;
@Autowired
private IdempotencyService idempotencyService;
/**
* 消费订单创建事件 - 扣减库存
*
* 注意:这里使用的是订单系统定义的OrderCreatedEvent,
* 库存系统不需要知道订单系统的内部实现,只需要理解事件的结构
*/
@StreamListener("order-event-input")
public void handleOrderCreated(OrderCreatedEvent event) {
// 幂等性检查 - 防止重复消费
if (idempotencyService.isProcessed(event.getEventId())) {
log.info("事件已处理过,跳过, eventId={}", event.getEventId());
return;
}
log.info("开始处理订单库存扣减, orderId={}, eventId={}",
event.getOrderId(), event.getEventId());
for (OrderItemDTO item : event.getItems()) {
// 扣减库存
boolean success = inventoryService.deductStock(
item.getProductId(),
item.getQuantity()
);
if (!success) {
// 库存不足,需要发退款消息
log.error("库存不足, productId={}, quantity={}",
item.getProductId(), item.getQuantity());
// 这里应该发布一个"库存不足"事件,通知订单系统
return;
}
}
// 标记事件已处理
idempotencyService.markProcessed(event.getEventId());
log.info("订单库存扣减完成, orderId={}", event.getOrderId());
}
}
// ===== 下游B:积分系统消费事件 =====
/**
* 积分消费者 - 接收订单创建事件,给会员加积分
*/
@Service
public class PointsOrderEventHandler {
private static final Logger log = LoggerFactory.getLogger(PointsOrderEventHandler.class);
@Autowired
private PointsService pointsService;
@Autowired
private IdempotencyService idempotencyService;
/**
* 消费订单创建事件 - 计算并添加积分
*
* 积分规则:每消费1元,得1积分
* 消费金额取整,不足1元不计
*/
@StreamListener("order-event-input")
public void handleOrderCreated(OrderCreatedEvent event) {
// 幂等性检查
if (idempotencyService.isProcessed(event.getEventId())) {
log.info("积分事件已处理过,跳过, eventId={}", event.getEventId());
return;
}
// 计算应得积分(100分 = 1元,所以除以100)
int points = (int) (event.getTotalAmountCents() / 100);
if (points > 0) {
pointsService.addPoints(
event.getUserId(),
points,
"订单消费积分",
event.getOrderId() // 关联订单ID,方便对账
);
log.info("积分添加完成, userId={}, points={}, orderId={}",
event.getUserId(), points, event.getOrderId());
}
idempotencyService.markProcessed(event.getEventId());
}
}
// ===== 下游C:通知系统消费事件 =====
/**
* 通知消费者 - 接收订单创建事件,给用户发送通知
*/
@Service
public class NotificationOrderEventHandler {
private static final Logger log = LoggerFactory.getLogger(NotificationOrderEventHandler.class);
@Autowired
private NotificationService notificationService;
@Autowired
private IdempotencyService idempotencyService;
/**
* 消费订单创建事件 - 发送通知
*/
@StreamListener("order-event-input")
public void handleOrderCreated(OrderCreatedEvent event) {
if (idempotencyService.isProcessed(event.getEventId())) {
return;
}
// 发送短信通知
String smsContent = String.format(
"您的订单[%s]已创建成功,订单金额%.2f元,我们将尽快为您处理。",
event.getOrderId(),
event.getTotalAmountCents() / 100.0
);
notificationService.sendSms(
event.getShippingInfo().getDeliveryPhone(),
smsContent
);
// 发送APP推送
notificationService.sendPush(
event.getUserId(),
"订单创建成功",
smsContent
);
idempotencyService.markProcessed(event.getEventId());
log.info("通知发送完成, orderId={}, phone={}",
event.getOrderId(),
event.getShippingInfo().getDeliveryPhone());
}
}
/*
* 发布语言模式的优点:
* 1. 完全解耦 - 上下游不需要实时通信,也不需要知道对方的存在
* 2. 灵活扩展 - 新增下游只需要订阅事件,不需要改上游代码
* 3. 容错性好 - 下游挂了不影响上游,消息可以暂存在队列里
* 4. 异步处理 - 适合不需要实时反馈的场景(发通知、扣库存、加积分)
*
* 缺点:
* 1. 一致性不是实时的 - 存在短暂的"最终一致性"窗口
* 2. 需要处理消息幂等性 - 同一条消息可能被消费多次
* 3. 调试比较麻烦 - 链路长,出问题需要查消息、查队列
* 4. 需要额外维护消息基础设施(Kafka/RabbitMQ等)
*
* 最佳实践建议:
* - 事件必须包含eventId用于幂等性检查
* - 事件必须包含occurredAt时间戳
* - 事件版本要管理好,老版本要兼容
* - 关键业务要考虑"消息丢了怎么办"(如:死信队列、定时对账)
* - 不要用事件传递大量数据,只传关键信息,下游自己去查详情
*/
十、实际应用场景分析
10.1 电商系统中的应用
电商系统是限界上下文映射最典型的应用场景。一个典型的电商平台包含以下限界上下文:
- 用户中心:管理用户注册、登录、身份认证
- 商品中心:管理商品上架、下架、价格
- 订单中心:管理订单创建、状态流转
- 支付中心:管理支付渠道、交易对账
- 库存中心:管理库存扣减、补货
- 营销中心:管理优惠券、促销活动
- 物流中心:管理发货、配送、签收
这些上下文之间的映射关系非常复杂。订单中心既要依赖用户中心(获取用户信息),又要依赖商品中心(获取商品信息),还要通知库存中心(扣减库存)和支付中心(发起支付)。不同关系要用不同的映射模式:
- 订单->用户中心:使用防腐层模式(因为用户中心数据结构复杂,需要转换)
- 订单->商品中心:使用开放主机服务模式(因为多个下游都要用商品数据)
- 订单->库存中心:使用发布语言模式(因为扣库存不需要实时反馈)
- 订单->支付中心:使用客户供应商模式(因为支付系统需要订单提供的特定格式数据)
10.2 金融系统中的应用
金融系统对数据一致性要求极高,稍有不慎就可能造成资金损失。典型的金融系统限界上下文包括:
- 账户服务:管理账户余额、开户销户
- 交易服务:管理转账、汇款、结算
- 风控服务:管理风险评估、反欺诈
- 账务服务:管理会计分录、日终对账
- 清算服务:管理批量清算、跨行清算
在金融系统中:
- 账户->账务:通常使用共享内核模式(因为它们对"金额"的理解必须完全一致)
- 交易->风控:使用防腐层模式(风控系统有自己的评估模型,需要隔离)
- 交易->账务:使用发布语言模式(交易完成后异步生成会计分录)
金融系统特别需要注意的是:所有涉及资金的事件都要做幂等性检查,所有消息都要持久化,所有操作都要有审计日志。
10.3 医疗系统中的应用
医疗系统的特点是对数据准确性和隐私性要求都很高。典型的医疗限界上下文包括:
- 患者服务:管理患者基本信息、病历
- 挂号服务:管理挂号、分诊
- 诊疗服务:管理诊断、处方、医嘱
- 药房服务:管理药品库存、处方审核
- 收费服务:管理费用计算、医保结算
在医疗系统中:
- 诊疗->患者服务:使用防腐层模式(患者数据格式复杂,诊疗只需要部分信息)
- 诊疗->药房:使用客户供应商模式(药房需要处方系统的特定格式)
- 挂号->患者服务:使用开放主机服务模式(多个系统都要查患者信息)
医疗系统特别需要注意的是:患者隐私数据(如身份证号、手机号)在跨上下文传递时要做脱敏处理,并且在每个上下文中只保留必要的字段。
十一、技术优缺点分析
11.1 技术优点
限界上下文映射的核心价值在于解决跨服务的数据一致性问题。具体来说有以下几个优点:
第一,降低耦合度。 通过映射模式隔离上下游,一个上下文的变更不会直接影响另一个上下文。比如用户中心改了手机号字段名,如果订单系统用了防腐层模式,只要防腐层的转换逻辑跟着改一下就行,订单系统内部代码完全不需要动。
第二,提升可维护性。 每个上下文的逻辑独立,修改一个上下文不需要动其他上下文。出了问题也容易定位——数据在哪个上下文走样了,就查那个上下文。
第三,支持渐进式演进。 一开始可能所有系统都耦合在一起,随着业务发展,可以逐步拆分出新的上下文,然后用映射模式连接新旧系统。这样可以在不停机的情况下完成系统拆分。
第四,容错性更强。 特别是发布语言模式,下游系统挂了对上游没有任何影响。消息在队列里等着,下游恢复后继续消费就行。
11.2 技术缺点
当然,限界上下文映射不是万能的,也有明显的缺点:
第一,复杂度增加。 映射模式本身就增加了一层复杂度。防腐层要维护转换逻辑,发布语言要维护消息格式,共享内核要维护版本号。这些额外的维护工作会消耗团队精力。
第二,实时性下降。 发布语言模式和开放主机服务模式都会引入一定的延迟。如果业务对实时性要求很高(比如高频交易系统),这些模式可能不适用。
第三,调试难度增加。 当数据不一致时,排查问题需要追踪整条链路。从上游发消息,到中间件存储,到下游消费,每个环节都可能出问题。需要有完善的日志和监控体系来支撑。
第四,学习成本高。 DDD本身就不简单,限界上下文映射又是DDD里比较复杂的概念。团队需要花时间学习和理解这些模式,才能在项目中正确应用。
十二、注意事项与最佳实践
12.1 边界划分的原则
限界上下文的边界怎么划?这是很多人问的第一个问题。我的建议是:
- 以业务能力为单位划分。不要按技术组件划分(比如不要按数据库划分),而要按业务能力划分(用户管理是一个,订单管理是一个)。
- 以变更节奏为单位划分。变化快的和变化慢的不要放在一起,否则互相拖累。
- 以团队为单位划分。一个团队维护一个限界上下文,避免多人同时改一个上下文。
- 以数据模型为单位划分。如果两个功能的数据模型完全不同,就应该拆成两个上下文。
12.2 事件驱动与同步调用的选择
什么时候用同步调用(防腐层、客户供应商),什么时候用异步消息(发布语言)?这个选择取决于业务需求:
- 需要实时反馈的场景用同步调用。比如用户下单时需要立即知道库存够不够,这就需要同步调用库存中心。
- 不需要实时反馈的场景用异步消息。比如订单创建后给客服系统发通知,客服系统晚几分钟收到也没关系。
- 需要高可用的场景用异步消息。同步调用链太长,一个挂了全挂了;异步消息可以解耦,一个挂了不影响其他。
12.3 版本管理的考虑
无论用哪种映射模式,都要考虑版本管理。常见的做法有:
- 接口版本:在URL里加版本号,比如
/api/v1/products和/api/v2/products。 - 消息版本:在消息里加 version 字段,消费者根据版本选择处理逻辑。
- 字段兼容:新增字段时用 Optional 类型,删除字段前先标记为废弃,给下游留迁移时间。
/**
* 版本管理示例 - 接口版本号 + 消息版本号的组合使用
*/
@RestController
@RequestMapping("/api/v2/orders")
public class OrderControllerV2 {
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponseV2> getOrder(
@PathVariable String orderId) {
OrderResponseV2 response = OrderResponseV2.builder()
.orderId(orderId)
.version("2.0") // 响应里也带上版本号
.createdAt(LocalDateTime.now())
.build();
return ResponseEntity.ok(response);
}
}
/**
* 消息版本兼容示例 - 消费者根据版本处理不同格式
*/
public class OrderEventConsumer {
@StreamListener("order-event-input")
public void handleOrderEvent(OrderCreatedEvent event) {
switch (event.getEventVersion()) {
case "1.0":
// 旧版本处理逻辑
handleV1Event(event);
break;
case "2.0":
// 新版本处理逻辑(可能多了新字段)
handleV2Event(event);
break;
default:
// 未知版本,记录日志但不处理
log.warn("收到未知版本的事件: {}", event.getEventVersion());
break;
}
}
}
/*
* 版本管理的最佳实践:
* 1. 新版本接口不要直接替换旧版本,而是并存一段时间
* 2. 给下游充足的迁移时间(通常至少一个迭代周期)
* 3. 废弃的接口要有明确的下线时间,并在文档中说明
* 4. 消息格式变更时,先在消费者端支持新版本,再切换生产者
*/
12.4 数据一致性保障
跨上下文的数据一致性是所有映射模式都要面对的问题。不同的映射模式提供不同级别的一致性保障:
| 映射模式 | 一致性级别 | 说明 |
|---|---|---|
| 上下游模式 | 强一致性 | 直接调用,数据实时同步 |
| 客户供应商模式 | 强一致性 | 同步调用,但格式由下游定义 |
| 防腐层模式 | 强一致性 | 同步调用,转换层保证格式一致 |
| 共享内核模式 | 强一致性 | 共用代码,数据天然一致 |
| 开放主机服务 | 强/最终一致性 | 取决于是否用缓存 |
| 发布语言模式 | 最终一致性 | 异步处理,有短暂延迟 |
如果你的业务对一致性要求很高(比如资金交易),优先选择同步调用模式。如果对实时性要求不高,发布语言模式是更好的选择。
十三、文章总结
13.1 核心要点回顾
限界上下文映射是DDD里解决跨系统数据一致性的核心手段。总结起来,有以下几个要点:
第一,限界上下文是"业务边界",不同上下文对同一个概念的理解可能不同,这是正常的。
第二,映射模式有很多种,每种模式对应不同的协作关系和一致性策略。上下游模式最简单但耦合最高,防腐层模式最实用但维护成本最高,发布语言模式解耦最好但调试最麻烦。
第三,选择哪种模式取决于业务场景。需要实时性的用同步模式,可以异步的用消息模式。跨团队协作建议用防腐层或开放主机服务,同一个团队的紧密耦合模块可以用共享内核。
第四,无论选哪种模式,都要做好版本管理和幂等性检查。数据在传递过程中一定会遇到各种意外情况,提前想好怎么处理。
13.2 实践建议
如果你正在实践中遇到跨系统数据一致性问题,我的建议是:
- 先不要急着上微服务。限界上下文映射可以在单体应用里实践,先把业务边界画清楚。
- 从最简单的模式开始。先用上下游模式跑通,发现耦合问题再升级到防腐层。
- 防腐层是万金油。不确定用哪个模式的时候,先上防腐层,后续可以调整。
- 发布语言模式适合"发出去就不用管"的场景。扣库存、发通知、加积分都是典型的发布语言场景。
- 共享内核要谨慎。只在非常核心、非常稳定的部分使用,不要什么都往里塞。
最后,限界上下文映射不是一天就能做好的。它需要团队对业务有深入的理解,需要持续的架构演进。但一旦做好,系统的可维护性和可扩展性会有质的提升。
评论
围绕“领域驱动设计限界上下文映射:解决跨上下文交互的一致性问题”参与讨论