程序员鸭梨头像
关注

微服务中的设计模式:从策略模式到事件溯源,架构演进的实用指南

微服务中的设计模式:从策略模式到事件溯源,架构演进的实用指南

一、微服务的模式困境:不是缺模式,而是选错模式

设计模式在单体应用中是代码组织工具,在微服务中则上升为架构决策。一个策略模式在单体中只是多态替换,在微服务中可能意味着服务拆分边界;一个观察者模式在单体中是事件回调,在微服务中可能演变为事件驱动架构。模式选错的代价也从"重构几行代码"升级为"重新设计服务边界"。

常见的选错模式包括:在需要最终一致性的场景中强行使用 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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--