跳转到正文
zeno's blog
返回

DDD(二):领域事件

专题: DDD

Table of contents

Open Table of contents

定义

领域事件是系统里已经发生的一个事实,用过去时描述。它的本质作用是解耦聚合之间的协作。

没有领域事件时的耦合问题

fn checkout(cart_id: CartId) {
    let cart = cart_repo.get(cart_id);
    let order = Order::create(cart.items);
    order_repo.save(order);
    payment_service.charge(order.total);    // 直接调用
    inventory_service.deduct(cart.items);   // 直接调用
    cart_repo.delete(cart_id);              // 直接调用
}

CheckoutService 必须知道支付、库存、购物车三个上下文的存在,改任何一个都要改这里。

有领域事件后

fn checkout(cart_id: CartId) {
    let cart = cart_repo.get(cart_id);
    let order = Order::create(cart.items);
    order_repo.save(order);
    event_bus.publish(OrderCreated { order_id, items, total });
    // 完了,不管别人怎么处理
}

// 支付上下文自己监听
fn on_order_created(event: OrderCreated) {
    payment_service.charge(event.total);
}

// 库存上下文自己监听
fn on_order_created(event: OrderCreated) {
    inventory_service.deduct(event.items);
}

// 购物车上下文自己监听
fn on_order_created(event: OrderCreated) {
    cart_repo.clear(event.customer_id);
}

CheckoutService 只负责创建订单和发事件,不知道谁在听、有几个人在听。以后加”发邮件通知买家”的需求,加一个 listener 就行,不碰结算代码。

领域事件就是聚合之间的通信协议:“我做完了我的事,谁关心谁自己来拿。“

业界实现

大厂的领域事件 = 分布式消息队列。概念完全一样,基础设施不同。

公司消息队列说明
阿里/淘宝RocketMQ(自研后开源)为了解决订单→支付→库存→物流的事件驱动而造的
京东JMQ(自研)+ Kafka订单履约链路全靠消息驱动
美团Kafka + 自研 Mafka外卖下单→商家接单→骑手派单,每步都是事件
海外Kafka / Pulsar / SQSAmazon、Uber、Netflix 都是事件驱动架构

下单后淘宝页面显示”订单已创建”,但支付、扣库存、通知商家是异步发生的——这就是领域事件在起作用。偶尔看到”库存扣减中”或者下单成功但几秒后告诉你”库存不足已取消”,这就是最终一致性的外在表现。

简化版 vs 生产版

简化版(进程内):
  OrderCreated → 进程内函数调用 → 同步执行

生产版(分布式):
  OrderCreated → 序列化成消息 → 发到 RocketMQ/Kafka
              → 支付服务消费这条消息(独立进程、可能不同机器)
              → 库存服务消费这条消息(独立进程、可能不同机房)
              → 失败了可以重试、可以回溯、消息不丢

选型建议

单体应用用进程内 event bus 就够了。将来拆服务时把 event bus 换成消息队列,业务代码几乎不用改——发布/监听的接口一样,只是传输层不同。这也是领域事件这个抽象的价值:不绑定具体基础设施。


分享这篇文章:

上一篇
tokio(一):运行时-从 mio 到异步运行时的完整设计
下一篇
DDD(一):领域驱动设计概览