微服务中的设计模式:从策略模式到事件溯源,架构演进的实用指南
一、微服务的模式困境:不是缺模式,而是选错模式
设计模式在单体应用中是代码组织工具,在微服务中则上升为架构决策。一个策略模式在单体中只是多态替换,在微服务中可能意味着服务拆分边界;一个观察者模式在单体中是事件回调,在微服务中可能演变为事件驱动架构。模式选错的代价也从"重构几行代码"升级为"重新设计服务边界"。
常见的选错模式包括:在需要最终一致性的场景中强行使用 Saga 而非事件溯源;在聚合根边界不清晰时引入 CQRS 导致数据不一致;在服务间通信简单的场景中过度使用事件总线。选对模式的前提是理解每个模式解决的问题和引入的代价。
二、微服务设计模式的层次结构:从代码级到架构级
微服务中的设计模式可以分为三个层次:代码级模式(策略、工厂、模板方法)解决单个服务内的代码组织问题;集成级模式(适配器、外观、代理)解决服务间的接口适配问题;架构级模式(Saga、事件溯源、CQRS)解决分布式数据一致性问题。
flowchart TB
subgraph 架构级模式
A[Saga 模式<br/>分布式事务编排]
B[事件溯源<br/>状态变更即事件]
C[CQRS<br/>读写分离]
end
subgraph 集成级模式
D[适配器模式<br/>协议转换]
E[外观模式<br/>接口聚合]
F[代理模式<br/>远程调用封装]
end
subgraph 代码级模式
G[策略模式<br/>算法族替换]
H[工厂模式<br/>对象创建解耦]
I[模板方法<br/>流程骨架复用]
end
A --> D
B --> E
C --> F
D --> G
E --> H
F --> I
架构级模式的选择决定了服务的边界和通信方式。Saga 模式适合长事务场景(如订单流程),事件溯源适合审计追踪场景(如金融交易),CQRS 适合读写负载差异大的场景(如商品详情页)。三者可以组合使用,但每增加一个模式,系统复杂度就上一个台阶。
三、生产级代码实现:策略模式 + Saga 编排
3.1 策略模式:支付方式动态选择
// 策略接口:不同支付方式的统一抽象
// 为什么用策略模式而非 if-else:支付方式会持续增加,
// if-else 每增加一种支付方式就要修改核心逻辑,
// 违反开闭原则;策略模式只需新增实现类
public interface PaymentStrategy {
boolean supports(PaymentType type);
PaymentResult pay(PaymentRequest request);
}
@Service
public class PaymentService {
private final List<PaymentStrategy> strategies;
public PaymentService(List<PaymentStrategy> strategies) {
// Spring 自动注入所有策略实现
this.strategies = strategies;
}
public PaymentResult processPayment(PaymentRequest request) {
PaymentStrategy strategy = strategies.stream()
.filter(s -> s.supports(request.getType()))
.findFirst()
.orElseThrow(() -> new BusinessException(
"不支持的支付方式: " + request.getType()));
return strategy.pay(request);
}
}
@Component
public class WechatPayStrategy implements PaymentStrategy {
private final WechatPayClient wechatClient;
@Override
public boolean supports(PaymentType type) {
return type == PaymentType.WECHAT;
}
@Override
public PaymentResult pay(PaymentRequest request) {
try {
WechatPayResponse resp = wechatClient
.createOrder(request.getOrderId(),
request.getAmount());
return PaymentResult.success(resp.getPrepayId());
} catch (WechatPayException e) {
// 微信支付异常需要区分可重试和不可重试
if (e.isRetryable()) {
throw new RetryablePaymentException(
"微信支付可重试异常", e);
}
return PaymentResult.fail(e.getMessage());
}
}
}
3.2 Saga 编排模式:订单创建流程
// Saga 编排器:管理分布式事务的步骤和补偿
// 为什么用编排而非协调:订单流程的步骤是固定的,
// 编排器集中管理状态转换和补偿逻辑,便于追踪和调试;
// 协调模式(事件驱动)步骤分散在各服务中,
// 流程不可见,排查困难
@Component
public class OrderSagaOrchestrator {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
private final SagaStateRepository sagaStateRepository;
public SagaResult execute(OrderRequest request) {
String sagaId = UUID.randomUUID().toString();
SagaState state = SagaState.builder()
.sagaId(sagaId)
.orderId(request.getOrderId())
.currentStep(SagaStep.INVENTORY_DEDUCT)
.build();
try {
// Step 1: 扣减库存
inventoryService.deduct(request.getSku(),
request.getQuantity());
state.setCurrentStep(SagaStep.PAYMENT);
sagaStateRepository.save(state);
// Step 2: 支付
PaymentResult payResult = paymentService.processPayment(
new PaymentRequest(request.getOrderId(),
request.getAmount()));
if (!payResult.isSuccess()) {
throw new PaymentFailedException(
payResult.getMessage());
}
state.setCurrentStep(SagaStep.SHIPPING);
sagaStateRepository.save(state);
// Step 3: 创建物流单
shippingService.createShipment(request.getOrderId());
state.setCurrentStep(SagaStep.COMPLETED);
sagaStateRepository.save(state);
return SagaResult.success(sagaId);
} catch (Exception e) {
log.error("Saga 执行失败: sagaId={}, step={}",
sagaId, state.getCurrentStep(), e);
compensate(state);
return SagaResult.fail(sagaId, e.getMessage());
}
}
private void compensate(SagaState state) {
// 按反向顺序执行补偿操作
// 为什么反向:正向执行的步骤有依赖关系,
// 反向补偿必须先撤销后执行的步骤
switch (state.getCurrentStep()) {
case PAYMENT, SHIPPING -> {
// 支付失败或物流失败,需要回滚库存
try {
inventoryService.restore(
state.getOrderId());
} catch (Exception e) {
log.error("库存回滚失败", e);
// 记录补偿失败,人工介入
sagaStateRepository.markCompensationFailed(
state.getSagaId());
}
}
case SHIPPING -> {
// 物流创建成功但后续失败,需要取消物流
try {
shippingService.cancelShipment(
state.getOrderId());
} catch (Exception e) {
log.error("物流取消失败", e);
}
// 继续回滚支付和库存
try {
paymentService.refund(state.getOrderId());
inventoryService.restore(
state.getOrderId());
} catch (Exception e) {
log.error("支付退款或库存回滚失败", e);
sagaStateRepository.markCompensationFailed(
state.getSagaId());
}
}
default -> log.info("无需补偿: step={}",
state.getCurrentStep());
}
}
}
3.3 事件溯源模式:账户余额变更
// 事件存储:所有状态变更以事件形式持久化
// 为什么用事件溯源而非 CRUD:金融账户的余额变更
// 需要完整审计追踪,CRUD 模式只保留最终状态,
// 无法回溯历史变更;事件溯源天然支持审计和时间旅行
@Entity
public class AccountEvent {
@Id private String eventId;
private String accountId;
private String eventType; // DEPOSIT, WITHDRAW, TRANSFER
private BigDecimal amount;
private Instant timestamp;
private String traceId;
}
@Service
public class AccountEventStore {
private final AccountEventRepository eventRepository;
public void append(String accountId, String eventType,
BigDecimal amount, String traceId) {
AccountEvent event = new AccountEvent();
event.setEventId(UUID.randomUUID().toString());
event.setAccountId(accountId);
event.setEventType(eventType);
event.setAmount(amount);
event.setTimestamp(Instant.now());
event.setTraceId(traceId);
// 事件不可变,只追加不修改
eventRepository.save(event);
}
public BigDecimal computeBalance(String accountId) {
// 通过回放所有事件计算当前余额
// 为什么不缓存余额:事件溯源的核心是"事件即真相",
// 缓存余额是 CQRS 的职责,事件存储只负责事实记录
List<AccountEvent> events = eventRepository
.findByAccountIdOrderByTimestampAsc(accountId);
return events.stream()
.reduce(BigDecimal.ZERO,
(balance, event) -> switch (event.getEventType()) {
case "DEPOSIT" -> balance.add(event.getAmount());
case "WITHDRAW" -> balance.subtract(
event.getAmount());
case "TRANSFER_OUT" -> balance.subtract(
event.getAmount());
case "TRANSFER_IN" -> balance.add(
event.getAmount());
default -> balance;
},
BigDecimal::add);
}
}
四、模式组合的架构权衡:复杂度、一致性与可调试性
Saga 补偿的幂等性要求:补偿操作必须是幂等的——网络超时可能导致补偿被重复执行。库存回滚接口需要检查是否已经回滚过,支付退款接口需要检查退款状态。幂等性增加了每个补偿操作的实现复杂度,但这是分布式事务的硬性要求。
事件溯源的事件版本演化:业务变化会导致事件结构变化(如增加字段、修改枚举值)。旧事件必须能被新代码正确处理,否则无法回放历史。建议在事件中增加版本号字段,并在反序列化时做版本适配(Upcasting)。事件版本管理是事件溯源最容易被低估的复杂度来源。
CQRS 的数据一致性延迟:读写分离后,读模型的数据更新是异步的,存在一致性延迟窗口。用户刚提交了修改,立即查询可能看到旧数据。解决方案是在写操作返回后,前端轮询读模型直到数据一致,但这增加了请求延迟。一致性延迟的容忍度需要与业务方明确约定。
模式组合的边际收益递减:Saga + 事件溯源 + CQRS 的组合能解决最复杂的分布式数据问题,但系统复杂度指数级增长。大多数业务场景只需要其中 1-2 个模式。建议从最简单的方案开始,只在明确遇到瓶颈时才引入更重的模式。
五、总结
微服务中的设计模式选择应遵循"最小复杂度"原则——用最简单的模式解决当前问题,不为未来可能的需求预支复杂度。策略模式解决代码扩展性问题,Saga 解决分布式事务问题,事件溯源解决审计追踪问题,CQRS 解决读写分离问题。模式之间可以组合,但每增加一个模式都要评估其引入的运维成本和排查难度。生产环境中,模式选错比不用模式更危险。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/alex_goden/article/details/161999930



