← 返回题目列表

观察者模式如何处理事务提交后的事件一致性?

高频 困难 第 14 / 27 题 更新于 2026/08/02
观察者模式事务一致性Spring事件可靠事件

简化版

事务场景下发布事件要关注“事件何时可见”和“监听器读到的数据是否已提交”。如果在事务未提交前同步通知,监听器可能读到未提交数据,甚至外部动作已经执行但主事务随后回滚。Spring 中可用 @TransactionalEventListener(phase = AFTER_COMMIT) 在提交后处理;跨进程可靠通知则通常使用本地消息表、Outbox 或消息队列。

详细版

订单创建成功后发布 OrderCreatedEvent,监听器可能发送短信、发放积分、通知仓库。如果事件在数据库事务提交前就被处理,会出现危险:

事务开始
  保存订单
  发布事件 -> 短信已发送
事务回滚
数据库里没有订单,但用户收到了短信

所以事务事件要先回答 3 个问题:

  1. 监听器是否需要看到已提交数据;
  2. 外部副作用能否跟随事务回滚;
  3. 事件是否需要可靠投递到其他服务。

应用内场景可用事务提交后监听;分布式场景要用可靠事件机制,而不是只靠本地观察者。

记忆钩子:事务内发布事件,最怕“外部世界已经响应,数据库最后回滚”。

完整版教学

面试提示:事务一致性题要先区分“事务内同步观察者”和“事务后异步观察者”,再谈 outbox、重试和补偿。

一、事务内同步事件有什么风险

看一个常见代码:

@Transactional
public void createOrder(CreateOrderCommand command) {
    Order order = orderRepository.save(command);
    eventPublisher.publishEvent(new OrderCreatedEvent(order.id()));
}

如果默认同步监听,监听器会在事务方法返回前执行。此时数据库事务可能还没提交。

风险包括:

  • 监听器查询订单,可能受隔离级别影响读不到;
  • 监听器调用外部系统,外部动作无法随数据库回滚;
  • 监听器抛异常,可能让主事务回滚;
  • 主事务最终失败,但事件副作用已经发生。

二、提交后监听适合应用内事件

Spring 提供 @TransactionalEventListener

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderCreated(OrderCreatedEvent event) {
    smsService.send(event.orderId());
}

AFTER_COMMIT 表示只有当前事务成功提交后,监听器才执行。这能避免“事务回滚但短信已发”的问题。

常见 phase:

phase含义场景
BEFORE_COMMIT提交前校验、准备数据
AFTER_COMMIT提交后发送通知、异步任务
AFTER_ROLLBACK回滚后清理、补偿
AFTER_COMPLETION完成后统计、释放资源

三、提交后监听也不是可靠消息

AFTER_COMMIT 只解决事务时机,不保证进程崩溃时事件一定送达。

例如:

事务提交成功
进程还没执行监听器就宕机
短信没有发送

如果业务要求可靠投递,就不能只靠本地内存事件。需要本地消息表、Outbox 或消息队列。

四、本地消息表如何保证可靠性

常见方案是业务数据和事件记录写在同一个事务里:

@Transactional
public void createOrder(CreateOrderCommand command) {
    Order order = orderRepository.save(command);
    eventLogRepository.save(new EventLog("OrderCreated", order.id()));
}

事务提交后,后台任务扫描未发送事件,投递到 MQ 或调用下游:

业务表 Order
事件表 EventLog(status=NEW)
  -> relay task
  -> MQ / downstream
  -> status=SENT

这样即使进程崩溃,事件表里仍然有待发送记录。

五、Outbox 模式适合跨服务事件

Outbox 和本地消息表思想相近:业务服务在同一数据库事务内写业务表和 outbox 表,再由 relay 组件发布消息。

优点:

  • 避免数据库提交成功但消息发送失败;
  • 可重试;
  • 可追踪;
  • 可做幂等消费。

代价是增加事件表、扫描任务、消息状态和清理机制。

六、监听器读数据要注意时机

如果事件只带 orderId,监听器通常要查数据库。提交前监听可能查不到或查到旧数据;提交后监听更稳。

如果事件携带完整快照,也要注意快照是否与最终提交一致。高频面试回答可以这样说:事件里带最小必要字段和版本号,监听器在提交后按 id 读取权威数据,必要时使用事件快照做审计。

七、事务事件要考虑幂等

可靠事件通常意味着可能重复投递。监听器必须幂等:

if (processedEventRepository.exists(event.id(), listenerName)) {
    return;
}
sendSms(event.orderId());
processedEventRepository.save(event.id(), listenerName);

尤其是发券、加积分、扣库存、发送外部请求,都要有业务唯一键或处理记录。

八、常见误区与追问

  • 误区:在事务方法里 publishEvent 就等于事务提交后发布。 默认同步事件通常会在事务提交前执行监听器。
  • 误区:AFTER_COMMIT 就能保证消息不丢。 它只保证执行时机,不能抵抗进程崩溃。
  • 误区:事件监听器读不到数据就是数据库坏了。 可能是事务还没提交,或者隔离级别导致不可见。
  • 误区:本地观察者可以替代消息队列。 跨进程可靠投递要用可靠事件表、Outbox 或 MQ。
  • 追问:什么时候用 @TransactionalEventListener 应用内、同进程、需要事务提交后执行的非核心副作用。
  • 追问:事务回滚后要做动作怎么办? 可以使用 AFTER_ROLLBACK,但要控制副作用和补偿边界。
  • 追问:可靠事件为什么需要幂等? 失败重试、网络抖动、消费者重启都可能导致重复处理。

九、加强记忆

  1. 事务内同步事件可能早于提交执行。
  2. 外部副作用无法随数据库事务自动回滚。
  3. Spring 可用 @TransactionalEventListener(AFTER_COMMIT) 控制时机。
  4. 提交后监听不等于可靠消息。
  5. 跨服务可靠事件用本地消息表、Outbox 或 MQ。
  6. 可靠投递一定要配幂等消费。