bigywater.tech
Spring · 142

@TransactionalEventListener 做 outbox

提交之后再发消息,用 @TransactionalEventListener 够不够?回滚时普通事件监听器会怎样?忘了加 @Transactional 会怎样?AFTER_COMMIT 里发送失败消息就丢了吗?发件箱怎么保证至少一次?这篇在 Spring Boot 4.0.8 和 MySQL 上逐项实测。

消息丢了
AFTER_COMMIT 监听器发送失败,订单已提交,place() 正常返回,调用方没有收到任何异常
静默忽略
没有事务的方法里发布事件,@TransactionalEventListener 不执行也不报错
p3 收到 2 次
relay 在发送成功后、标记前崩溃,重启后重复发送

执行顺序

实测 · 本机执行输出
  普通 @EventListener 立刻执行(此时事务还没结束):o1
  BEFORE_COMMIT 监听器:o1
  AFTER_COMMIT 监听器:o1,尝试发送到 broker
  AFTER_COMMIT 监听器:o1 已发送
  AFTER_COMPLETION 监听器:o1
  fallbackExecution=true 的监听器:o1(没有事务时也执行)
  orders 里有 o1:1 行;broker 收到:[o1]

普通监听器立刻执行;BEFORE_COMMIT → 提交 → AFTER_COMMIT → AFTER_COMPLETION。

回滚

实测 · 本机执行输出
  普通 @EventListener 立刻执行(此时事务还没结束):o2
  AFTER_ROLLBACK 监听器:o2
  AFTER_COMPLETION 监听器:o2
  调用方捕获:业务异常,回滚
  orders 里有 o2:0 行;broker 收到:[o1]

普通监听器副作用已发生;AFTER_COMMIT 不执行。

无事务

实测 · 本机执行输出
  fallbackExecution=true 的监听器:o3(没有事务时也执行)
  普通 @EventListener 立刻执行(此时事务还没结束):o3
  orders 里有 o3:1 行;broker 收到:[o1]

默认静默忽略,fallbackExecution 才执行。

发送失败

实测 · 本机执行输出
  普通 @EventListener 立刻执行(此时事务还没结束):o4
  BEFORE_COMMIT 监听器:o4
  AFTER_COMMIT 监听器:o4,尝试发送到 broker
  AFTER_COMPLETION 监听器:o4
  fallbackExecution=true 的监听器:o4(没有事务时也执行)
  place(o4) 正常返回,调用方没有收到任何异常
  orders 里有 o4:1 行;broker 收到:[o1](o4 没有发出去,也没有任何补偿)

订单已提交消息丢失,没有补偿。

AFTER_COMMIT 写库

实测 · 本机执行输出
  AFTER_COMMIT 里直接用 JdbcTemplate 写了 audit 行 a1(没有 REQUIRES_NEW)
  AFTER_COMMIT 里在 REQUIRES_NEW 事务里写了 audit 行 a2
  audit 表里的行:[{id=a1, note=same-tx}, {id=a2, note=new-tx}]

文档要求 REQUIRES_NEW;我的 JDBC 环境里碰巧也保存了。

发件箱

实测 · 本机执行输出
  orders 和 outbox 各新增了 p1、p2;outbox 未发送行数 2
  relay:发送 p1 失败(broker 不可用),保留在 outbox
  relay:发送 p2 失败(broker 不可用),保留在 outbox
  broker 不可用时 relay 发送了 0 条;outbox 未发送行数 2
  broker 恢复后 relay 发送了 2 条;outbox 未发送行数 0;broker 收到:[p1, p2]
  relay:发送 p3 成功,但在标记 sent 之前崩溃了
  模拟 relay 在发送后、标记前崩溃;broker 收到 [p3],outbox 未发送行数 1
  relay 重启后再发送一轮;broker 一共收到 [p3, p3](p3 收到了 2 次)

同事务写 outbox,relay 补发;至少一次,可能重复。

分步演示

@TransactionalEventListener
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。MySQL 是自己起的独立实例;只用 JdbcTemplate + DataSourceTransactionManager,没有 JPA;broker 用一个内存列表模拟,可以人为设成「不可用」;relay 是在代码里手工调用的,没有定时器
    • 没有验证:文档里关于 AFTER_COMMIT 必须用 REQUIRES_NEW 的警告我是凭记忆引用的,没有逐字核对原文;AFTER_COMMIT 里不加 REQUIRES_NEW 的写库为什么保存了(只观察到结果,原因是猜测);用 JPA(flush)时 AFTER_COMMIT 写库的表现;@Async 监听器的事务边界;发件箱的轮询间隔与 FOR UPDATE SKIP LOCKED 多实例并发;CDC(Debezium)方案;事件的顺序保证;Spring Modulith 的事件发布注册表(event publication registry);另一篇分布式事务系列中的 outbox(transaction-outbox)
    京ICP备2026067502号-1京公网安备11010502063487号