事件驱动架构(EDA):Event Sourcing 和 CQRS 的完整实践

开篇:为什么我们需要事件驱动?

想象一下,你在网上下了一个订单。传统的系统会这样处理:

订单状态:已创建 → 已支付 → 已发货 → 已完成

每次状态变更,数据库里的 order_status 字段就被覆盖一次。看起来很合理对吧?但有一天,客服打来电话:"这个订单为什么从已支付直接变成已完成,中间的发货记录去哪了?"你傻眼了,因为数据已经被覆盖,历史消失了。

这就是传统"状态存储"模式的痛点:我们只知道现在是什么样,却不知道是怎么变成这样的。

事件驱动架构(Event-Driven Architecture)换了一个思路:不存储状态,存储事件。就像银行流水账,每一笔交易都清清楚楚,最终余额是所有交易的累加结果。


一、事件驱动架构的核心理念

1.1 什么是事件?

事件是已经发生的事实,用过去式描述,不可篡改。

传统方式(状态):

Order order = new Order();
order.setStatus("PAID"); // 我现在是"已支付"状态

事件方式(事实):

OrderPaidEvent event = new OrderPaidEvent(
    orderId: "123",
    amount: 299.0,
    paidAt: "2025-10-16 10:30:00"
); // "订单在某个时间点被支付了" - 这是历史事实

1.2 事件驱动的三大优势

1. 完整的审计日志

  • 每个状态变化都有迹可循

  • 金融、医疗等强合规行业的刚需

2. 时间旅行(Time Travel)

  • 可以重放历史事件,回到任意时间点的状态

  • 出 Bug 时可以精确复现:"10月1日的订单数据是怎么算出来的?"

3. 解耦和可扩展

  • 发送事件的人不关心谁消费

  • 新业务接入只需订阅事件,不改老代码


二、Event Sourcing:用事件重建世界

2.1 核心思想

Event Sourcing(事件溯源)的核心:不存当前状态,存所有历史事件,状态通过回放事件计算出来。

类比:Git 的 commit 历史就是典型的 Event Sourcing,代码的当前状态是所有 commit 的累积结果。

2.2 实战:订单系统的事件溯源

传统方式的表结构:

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    status VARCHAR(20), -- 只保存最新状态
    total_amount DECIMAL(10,2)
);

Event Sourcing 方式:

CREATE TABLE order_events (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    aggregate_id VARCHAR(50), -- 订单ID
    event_type VARCHAR(100),   -- 事件类型
    event_data JSON,           -- 事件内容
    version INT,               -- 版本号(防并发)
    created_at TIMESTAMP
);

核心代码:聚合根(Aggregate Root)

public class Order {
    private String orderId;
    private OrderStatus status;
    private BigDecimal totalAmount;
    private List<DomainEvent> uncommittedEvents = new ArrayList<>();
    
    // 通过事件重建订单状态
    public static Order rebuild(List<DomainEvent> events) {
        Order order = new Order();
        for (DomainEvent event : events) {
            order.apply(event); // 逐个回放事件
        }
        return order;
    }
    
    // 业务方法:支付订单
    public void pay(BigDecimal amount) {
        if (this.status != OrderStatus.CREATED) {
            throw new IllegalStateException("订单状态不允许支付");
        }
        // 产生事件,而不是直接改状态
        OrderPaidEvent event = new OrderPaidEvent(orderId, amount, Instant.now());
        apply(event); // 应用到内存
        uncommittedEvents.add(event); // 待持久化
    }
    
    // 应用事件到当前状态(状态机)
    private void apply(DomainEvent event) {
        if (event instanceof OrderCreatedEvent) {
            OrderCreatedEvent e = (OrderCreatedEvent) event;
            this.orderId = e.getOrderId();
            this.status = OrderStatus.CREATED;
            this.totalAmount = e.getAmount();
        } else if (event instanceof OrderPaidEvent) {
            this.status = OrderStatus.PAID;
        } else if (event instanceof OrderShippedEvent) {
            this.status = OrderStatus.SHIPPED;
        }
        // ... 更多事件处理
    }
}

持久化事件(Repository 层):

public class OrderRepository {
    
    public void save(Order order) {
        List<DomainEvent> events = order.getUncommittedEvents();
        for (DomainEvent event : events) {
            // 插入事件表
            jdbcTemplate.update(
                "INSERT INTO order_events (aggregate_id, event_type, event_data, version) VALUES (?, ?, ?, ?)",
                order.getOrderId(),
                event.getClass().getName(),
                toJson(event),
                order.getVersion()
            );
        }
        // 发布到消息队列,供其他服务消费
        eventBus.publish(events);
        order.clearUncommittedEvents();
    }
    
    public Order findById(String orderId) {
        // 查询该订单的所有历史事件
        List<Map<String, Object>> rows = jdbcTemplate.queryForList(
            "SELECT * FROM order_events WHERE aggregate_id = ? ORDER BY version",
            orderId
        );
        List<DomainEvent> events = rows.stream()
            .map(this::toEvent)
            .collect(Collectors.toList());
        return Order.rebuild(events); // 通过事件重建订单
    }
}

2.3 快照优化:别每次都回放所有事件

想象一个订单有 10000 条事件,每次查询都回放 10000 次?性能爆炸!

解决方案:快照(Snapshot)

  • 每隔 N 个事件(如 100 个)保存一次完整状态

  • 查询时:加载最近的快照 + 之后的增量事件

public Order findById(String orderId) {
    // 1. 查询最新快照
    OrderSnapshot snapshot = snapshotRepository.findLatest(orderId);
    Order order = snapshot != null ? snapshot.toOrder() : new Order();
    
    // 2. 只回放快照之后的事件
    int fromVersion = snapshot != null ? snapshot.getVersion() : 0;
    List<DomainEvent> events = eventRepository.findByIdAndVersionGreaterThan(orderId, fromVersion);
    
    for (DomainEvent event : events) {
        order.apply(event);
    }
    return order;
}

2.4 Event Sourcing 的挑战

挑战

解决方案

查询复杂

引入 CQRS,建立专门的查询模型

数据迁移

通过"升级事件"(Upcasting)处理历史事件

事件版本演进

事件携带版本号,兼容多版本

删除数据困难

GDPR 等隐私法规可用"遗忘事件"标记


三、CQRS:读写分离的终极形态

3.1 什么是 CQRS?

CQRS(Command Query Responsibility Segregation,命令查询职责分离):读和写用不同的模型。

传统方式:

// 同一个 Order 对象既用来写,也用来读
Order order = orderService.getById(123); // 查询
order.setStatus("PAID");                 // 修改
orderService.update(order);              // 保存

CQRS 方式:

// 写模型:聚合根,只处理命令
OrderAggregate order = commandService.load(123);
order.pay(amount);
commandService.save(order);

// 读模型:专门为查询优化的视图
OrderQueryView view = queryService.getOrderDetail(123); // 快速查询

3.2 为什么需要 CQRS?

痛点 1:读写需求矛盾

  • 写操作:需要强一致性、事务、复杂业务逻辑

  • 读操作:需要高性能、多维度查询、统计分析

一个模型很难同时满足两者。

痛点 2:复杂查询拖累性能

// 传统 ORM 的"对象-关系阻抗"
@Entity
public class Order {
    @OneToMany
    private List<OrderItem> items; // 每次查询都关联查询?
    
    @ManyToOne
    private Customer customer; // N+1 问题
}

CQRS 的解法:

  • 写模型:规范化存储(Event Sourcing 事件表)

  • 读模型:反规范化存储(宽表、ES、Redis)

3.3 实战:CQRS 架构设计

整体架构:

命令端(写)                    查询端(读)
┌─────────────┐               ┌──────────────┐
│ Command API │──写──>│事件表│──事件──>│ Projection │──>│查询数据库│
└─────────────┘               └──────────────┘
     │                              │
     └──────────────────────────────┘
              异步同步

命令端(Command Side):

// 命令对象(携带意图)
public class PayOrderCommand {
    private String orderId;
    private BigDecimal amount;
    private String paymentMethod;
}

// 命令处理器
@Service
public class OrderCommandHandler {
    
    @Transactional
    public void handle(PayOrderCommand command) {
        // 1. 加载聚合根
        Order order = orderRepository.findById(command.getOrderId());
        
        // 2. 执行业务逻辑
        order.pay(command.getAmount());
        
        // 3. 保存事件
        orderRepository.save(order); // 写入 order_events 表
    }
}

查询端(Query Side):

// 查询视图(宽表设计)
@Entity
@Table(name = "order_query_view")
public class OrderQueryView {
    private String orderId;
    private String customerName;     // 冗余客户信息
    private String productNames;     // 冗余商品名称
    private BigDecimal totalAmount;
    private String statusText;       // 状态的可读文本
    private LocalDateTime createdAt;
}

// 投影器(Projector):消费事件,更新查询视图
@Component
public class OrderQueryProjector {
    
    @EventListener
    public void on(OrderPaidEvent event) {
        // 更新查询视图
        OrderQueryView view = queryRepository.findById(event.getOrderId());
        view.setStatusText("已支付");
        view.setPaidAt(event.getPaidAt());
        queryRepository.save(view);
    }
    
    @EventListener
    public void on(OrderShippedEvent event) {
        OrderQueryView view = queryRepository.findById(event.getOrderId());
        view.setStatusText("已发货");
        view.setShippedAt(event.getShippedAt());
        queryRepository.save(view);
    }
}

3.4 最终一致性的处理

CQRS 的代价是:命令端和查询端数据不同步。

场景:用户刚支付完,刷新页面还显示"待支付"

解决方案:

1. 前端轮询(简单粗暴)

// 支付成功后,每隔 500ms 查询一次状态,最多 10 次
for (let i = 0; i < 10; i++) {
    await sleep(500);
    const order = await api.getOrder(orderId);
    if (order.status === 'PAID') break;
}

2. 命令返回预期状态(乐观 UI)

public CommandResult handle(PayOrderCommand command) {
    // ... 执行命令
    return CommandResult.builder()
        .success(true)
        .expectedStatus("PAID") // 告诉前端预期状态
        .eventId(event.getId()) // 前端可用此 ID 对账
        .build();
}

3. WebSocket 推送(实时性要求高)

@EventListener
public void on(OrderPaidEvent event) {
    // 投影更新完成后,推送给前端
    webSocketService.send(event.getOrderId(), "状态已更新");
}

4. 版本号机制

public class OrderQueryView {
    private Long version; // 版本号,与命令端的事件版本对应
}

// 查询时验证版本
public OrderQueryView getOrder(String orderId, Long minVersion) {
    OrderQueryView view = queryRepository.findById(orderId);
    if (view.getVersion() < minVersion) {
        // 投影还没更新到最新,等待或返回提示
        throw new ViewNotReadyException("数据同步中...");
    }
    return view;
}


四、实战:电商系统的完整落地

4.1 领域模型设计

核心聚合根:

  • Order(订单)

  • Inventory(库存)

  • Payment(支付)

事件流:

1. 用户下单
   └─> OrderCreatedEvent
        ├─> 扣减库存(InventoryReservedEvent)
        └─> 创建支付单(PaymentCreatedEvent)

2. 用户支付
   └─> PaymentCompletedEvent
        └─> 订单确认(OrderConfirmedEvent)

3. 商家发货
   └─> OrderShippedEvent

4.2 事件设计的最佳实践

1. 事件命名:领域语言 + 过去式

// ❌ 糟糕的命名
UpdateOrderStatusEvent

// ✅ 好的命名
OrderPaidEvent
OrderShippedEvent
OrderCancelledEvent

2. 事件内容:包含完整上下文

public class OrderPaidEvent {
    private String orderId;
    private BigDecimal amount;
    private String paymentMethod; // 支付方式
    private String userId;
    private Instant paidAt;
    private String eventId; // 幂等性保证
}

3. 事件版本化

// V1 版本
public class OrderCreatedEventV1 {
    private String orderId;
    private BigDecimal amount;
}

// V2 版本(新增字段)
public class OrderCreatedEventV2 {
    private String orderId;
    private BigDecimal amount;
    private String currency; // 新增:货币类型
    
    // 提供升级方法
    public static OrderCreatedEventV2 upgrade(OrderCreatedEventV1 v1) {
        return new OrderCreatedEventV2(v1.getOrderId(), v1.getAmount(), "CNY");
    }
}

4.3 Saga 模式:分布式事务的事件编排

场景:下单流程跨多个服务

订单服务 → 库存服务 → 支付服务

如果支付失败,需要回滚库存和订单。

Saga 模式:通过事件串联多个本地事务

// 编排器(Saga Orchestrator)
@Component
public class CreateOrderSaga {
    
    public void start(CreateOrderCommand command) {
        // Step 1: 创建订单
        OrderCreatedEvent orderEvent = orderService.createOrder(command);
        
        try {
            // Step 2: 扣减库存
            InventoryReservedEvent inventoryEvent = inventoryService.reserve(orderEvent);
            
            // Step 3: 创建支付
            PaymentCreatedEvent paymentEvent = paymentService.create(inventoryEvent);
            
            // Step 4: 完成订单
            orderService.confirm(orderEvent.getOrderId());
            
        } catch (Exception e) {
            // 补偿事务(Compensating Transaction)
            inventoryService.release(orderEvent.getOrderId()); // 释放库存
            orderService.cancel(orderEvent.getOrderId());      // 取消订单
        }
    }
}


五、技术选型与工具链

5.1 事件存储(Event Store)

方案

优点

缺点

适用场景

MySQL

简单、事务支持

性能有限

中小型系统

EventStoreDB

专为 ES 设计

学习成本高

Event Sourcing 重度使用

Kafka

高吞吐、持久化

不支持随机查询

事件流转

Cassandra

海量数据

一致性较弱

超大规模

推荐组合:MySQL(事件存储)+ Kafka(事件分发)

5.2 开源框架

1. Axon Framework(Java)

  • 集成了 Event Sourcing + CQRS

  • 内置 Saga 支持

  • 社区活跃

2. EventFlow(.NET)

  • 轻量级

  • 适合快速上手

3. 自研(灵活性高)

  • 适合有特殊业务逻辑的团队

  • 上面的示例代码就是自研的简化版


六、踩坑与避坑指南

6.1 不是所有场景都适合 Event Sourcing

❌ 不适合的场景:

  • CRUD 为主的管理后台

  • 业务逻辑简单、不需要审计

  • 团队对事件驱动理解不足

✅ 适合的场景:

  • 金融交易(每笔记录都要追溯)

  • 物流追踪(状态变化频繁)

  • 协同编辑(如 Google Docs)

6.2 事件风暴(Event Storming)必不可少

什么是事件风暴?

  • 团队一起用便利贴贴墙,梳理业务流程中的所有事件

  • 识别领域边界、聚合根、命令

为什么重要?

  • Event Sourcing 的成败 80% 取决于事件设计

  • 事件设计错了,后期重构成本巨大

6.3 监控告警不能少

关键指标:

  • 事件延迟:命令端事件产生 → 查询端投影完成的时间

  • 投影失败率:消费事件时的异常比例

  • 事件回放耗时:系统恢复时需要回放多少事件

@Component
public class EventMetrics {
    
    @Timed("event.projection.duration")
    public void project(DomainEvent event) {
        try {
            // 投影逻辑
        } catch (Exception e) {
            meterRegistry.counter("event.projection.failed").increment();
            throw e;
        }
    }
}


七、进阶话题

7.1 事件溯源 + 区块链

事件不可篡改的特性天然适合区块链:

  • 每个事件作为一个区块

  • 通过哈希链保证数据完整性

  • 适用于多方协作、需要公信力的场景(供应链金融)

7.2 实时物化视图(Materialized View)

通过 Flink/Spark Streaming 实时消费事件,生成复杂的聚合视图:

// Flink 示例(伪代码)
events.keyBy(OrderEvent::getOrderId)
      .timeWindow(Time.days(1))
      .aggregate(new OrderStatisticsAggregator())
      .addSink(new ElasticsearchSink()); // 写入 ES 供查询

7.3 事件驱动的微服务通信

传统 RPC 调用:

订单服务 --RPC--> 库存服务 --RPC--> 支付服务

强耦合,任何一个服务挂了,整个流程卡住。

事件驱动:

订单服务 --发布事件--> 消息队列 <--订阅-- 库存服务、支付服务

各服务独立消费,解耦且高可用。


结语:从理念到落地的思维跃迁

事件驱动架构不只是技术选型,更是思维方式的转变:

  • 从"当前是什么"到"发生了什么"

  • 从"数据修改"到"事实追加"

  • 从"同步调用"到"异步响应"

小步快跑的落地路径:

  1. Phase 1:引入事件总线

    • 在现有系统中加入事件发布/订阅,服务间解耦

    • 工具:Spring Event、Guava EventBus

  2. Phase 2:关键聚合引入 Event Sourcing

    • 选择核心领域(如订单、账户)试点

    • 保留传统表作为快照/查询视图

  3. Phase 3:全面 CQRS

    • 读写彻底分离

    • 引入专业的 Event Store

记住:架构演进是渐进式的,不要一上来就推倒重来!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C_x_330

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值