本文目录导读:

在 PHP 中,领域事件(Domain Events) 和 集成事件(Integration Events) 是构建复杂业务系统(尤其是微服务架构或模块化单体架构)时用于解耦的核心概念,它们虽然名字相似,但目的、作用范围和发送方式截然不同。
以下是对两者的深度剖析及 PHP 实现示例。
领域事件(Domain Events)
核心概念
领域事件是在聚合根(Aggregate Root)内部发生重要业务变化时产生的事件,它描述的是“过去发生了什么”(订单已支付、库存已扣减),并且属于当前业务限界上下文(Bounded Context)内部。
核心目的
- 解耦聚合:在同一个进程/事务中,将“状态变更”(如修改订单状态)与“副作用”(如发送欢迎短信、更新统计表)分离开,避免在一个方法里写满 if-else。
- 保证事务一致性(:领域事件通常在数据库事务提交后才被分发,确保你看到的永远是最终一致且持久化的数据。
技术要点(PHP 实现)
领域事件通常是同步处理的(在同一次请求/事务内),或者通过简单的消息队列进行异步分发。
代码示例(无框架,纯 PHP 思路):
<?php
// 1. 定义事件对象
class OrderPaidEvent {
public function __construct(public readonly int $orderId, public readonly float $amount) {}
}
// 2. 事件分发器(简单的 DI 容器)
class EventDispatcher {
private array $listeners = [];
public function addListener(string $eventClass, callable $listener): void {
$this->listeners[$eventClass][] = $listener;
}
// 注意:通常在事务提交后调用,确保数据已落库
public function dispatch(object $event): void {
foreach ($this->listeners[get_class($event)] ?? [] as $listener) {
$listener($event); // 同步执行
}
}
}
// 3. 聚合根(实体)
class Order {
private array $domainEvents = [];
public function markAsPaid(): void {
$this->status = 'paid';
// 触发事件 - 先记录在内部
$this->domainEvents[] = new OrderPaidEvent($this->id, $this->total);
}
public function pullDomainEvents(): array {
$events = $this->domainEvents;
$this->domainEvents = []; // 清除
return $events;
}
}
// 4. 使用:在业务服务层
class OrderService {
public function __construct(private EventDispatcher $dispatcher) {}
public function payOrder(int $orderId): void {
// ... 伪代码:$order = $repo->find($orderId);
$order->markAsPaid();
// $repo->save($order); // 执行 UPDATE
// 关键点:事务提交后取出事件并调度
foreach ($order->pullDomainEvents() as $event) {
$this->dispatcher->dispatch($event);
}
}
}
// 5. 监听器
$dispatcher = new EventDispatcher();
$dispatcher->addListener(OrderPaidEvent::class, function (OrderPaidEvent $event) {
// 发送邮件通知(实际项目中这里可能调用另一个领域服务)
echo "Send invoice email for order: {$event->orderId}\n";
});
特点:
- 同进程内:通常不跨服务或跨数据库。
- 事务性:依赖“事务提交后”的钩子(如 Laravel 的
afterCommit)。 - 语言层面:强类型,类名即事件名。
集成事件(Integration Events)
核心概念
集成事件是用于在多个独立的限界上下文(服务)或外部系统之间进行通信的事件,它解决的是跨服务的数据同步或业务协作问题(订单服务通知库存服务扣减库存)。
核心目的
- 解耦微服务:不通过同步 API 调用来传递数据,而是通过事件驱动。
- 最终一致性:由于跨系统,无法保证强一致,只能保证最终数据一致(订单支付后,库存服务和财务服务各自异步处理)。
技术要点(PHP 实现)
集成事件通常必须通过消息队列中间件(如 RabbitMQ, Kafka, Redis Stream)异步发布,它不依赖应用内的数据库事务成功。
代码示例(结合 RabbitMQ 库 php-amqplib):
<?php
// 1. 定义集成事件(通常是 Serializable 的 DTO )
class OrderPaidIntegrationEvent implements JsonSerializable {
public function __construct(
public readonly string $eventId,
public readonly int $orderId,
public readonly string $customerEmail,
public readonly float $totalAmount,
public readonly \DateTimeImmutable $occurredOn
) {}
public function jsonSerialize(): array {
return [
'event_id' => $this->eventId,
'order_id' => $this->orderId,
'customer_email' => $this->customerEmail,
'total_amount' => $this->totalAmount,
'occurred_on' => $this->occurredOn->format(DATE_ATOM),
];
}
}
// 2. 发布者(在领域事件监听器中被调用)
class IntegrationEventPublisher {
public function __construct(private \PhpAmqpLib\Channel\AMQPChannel $channel) {}
public function publish(JsonSerializable $event, string $exchangeName): void {
$serialized = json_encode($event);
$msg = new \PhpAmqpLib\Message\AMQPMessage($serialized, [
'content_type' => 'application/json',
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
]);
// 发布到交换机,路由键可以是事件名
$this->channel->basic_publish($msg, $exchangeName, $event::class);
echo "Published integration event: " . $event::class . "\n";
}
}
// 3. 如何在业务中使用
class OrderService {
public function __construct(
private EventDispatcher $domainDispatcher,
private IntegrationEventPublisher $integrationPublisher
) {
// 监听领域事件,转换为集成事件
$this->domainDispatcher->addListener(
OrderPaidEvent::class,
function (OrderPaidEvent $domainEvent) {
// 将领域事件转换为集成事件(包含更多上下文信息)
$integrationEvent = new OrderPaidIntegrationEvent(
eventId: bin2hex(random_bytes(16)),
orderId: $domainEvent->orderId,
customerEmail: 'customer@example.com', // 这里需要从 DB 查询或附加数据
totalAmount: $domainEvent->amount,
occurredOn: new \DateTimeImmutable()
);
$this->integrationPublisher->publish($integrationEvent, 'order_exchange');
}
);
}
}
// 4. 订阅者(在另一个服务中,循环消费队列)
class OrderPaidConsumer {
public function handle(string $messageBody): void {
$data = json_decode($messageBody, true);
// 这里是 库存服务 或 财务服务
echo "Inventory service: Decrease stock for order {$data['order_id']}\n";
// 处理失败时记录日志,并自动重试或放入死信队列
}
}
特点:
- 跨进程/跨网络:通过中间件传输。
- 异步:发送方不需要等待结果。
- 容错性:需要实现重试、死信队列、幂等性处理(因为可能重复发送)。
核心区别对比表
| 维度 | 领域事件 (Domain Events) | 集成事件 (Integration Events) |
|---|---|---|
| 作用范围 | 单个限界上下文(服务)内部 | 跨限界上下文(多个服务)或多个系统 |
| 核心目的 | 解耦聚合根,处理内部副作用 (Side effects) | 解耦服务,实现最终一致性 (Eventually Consistent) |
| 传递方式 | 进程内同步调用(常配合事务提交钩子) | 跨进程异步消息 (MQ / Kafka / Redis Stream) |
| 事务关联 | 与本地数据库事务强相关(提交后才发送) | 与数据库事务无关,只负责将事件发送到 Broker |
| 数据完整性 | 依赖数据库 ACID | 依赖消息队列的持久化、回执机制 |
| 事件命名 | 过去时态,动词+名词+ed (如 OrderCreated) |
通常包含“集成”含义或更抽象的领域词 |
| 失败处理 | 失败通常导致整体事务回滚(同步处理) | 失败通常不阻塞主流程,需异步重试/降级 |
| 监听者 | 同一服务的其他模块代码 | 其他服务的消费者 (Consumer) |
实战中的流程串联
在复杂的 PHP 应用(如 Laravel + RabbitMQ)中,典型流程如下:
- 业务操作触发:用户点击“支付”,调用
OrderService::pay()。 - 内部状态变更:订单聚合根修改状态为
paid,并生成领域事件OrderPaid。 - 持久化:保存订单到 DB(事务开启 -> 更新订单 -> 提交事务)。
- 领域事件分发(事务提交后):
- A. 内部监听:向用户发送“支付成功”邮件(同步,若失败则本次请求失败,但订单已存)。
- B. 发布集成事件:将
OrderPaid映射为集成事件OrderPaidIntegrationEvent,发布到 RabbitMQ 的OrderExchange。
- 异步消费(库存服务):
- 监听
OrderPaidIntegrationEvent。 - 扣减库存(在自己的数据库中执行事务)。
- 如果库存不足,发送
InventoryNotEnough事件,通知订单服务作废订单。
- 监听
注意事项与最佳实践
- 不要在领域事件里放敏感数据:如银行卡号,集成事件只是通知信号,具体数据应通过 API 查询获取(或只放
orderId)。 - 幂等性:消费集成事件时必须检查是否已处理(例如通过
eventId查本地表),防止 MQ 重复发送导致数据错误。 - 版本管理:实体类(如
Order)的变更不应破坏已发布的集成事件结构,建议使用 DTO(Data Transfer Object)模式。 - 使用事件存储(Event Sourcing)时:领域事件可以被持久化,而集成事件则没有必要持久化(可通过 MQ 死信队列保留)。
- 领域事件是 “内部通知”,用来清理代码结构,属于同一个“家”(服务)。
- 集成事件是 “跨墙信息”,用来让别的系统知道你的变化,属于“广播”。
在 PHP 实践中,通常会先使用 symfony/event-dispatcher(或 Laravel Events)处理领域事件,再在其监听器中使用 enqueue、php-amqplib 或 laminas 发布集成事件到 MQ。