新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring 的事件机制你用了三年,但 @TransactionalEventListener 的坑一个都没绕过去

发布时间:2026/9/2 7:47:36来源:尧图网络
Spring 的事件机制你用了三年,但 @TransactionalEventListener 的坑一个都没绕过去
Spring 的事件机制你用了三年但 TransactionalEventListener 的坑一个都没绕过去事情是这样的用户下单后要发短信通知。我在 OrderService 里写了个EventListenerjava Service public class OrderService {Autowired private ApplicationEventPublisher publisher; Transactional public void createOrder(OrderDTO dto) { Order order orderRepo.save(dto.toEntity()); publisher.publishEvent(new OrderCreatedEvent(order)); // 发短信 smsService.send(order.getUserPhone(), 下单成功); }} 测试环境一切正常。上线当天客服电话被打爆——我下单成功了但没收到短信。查日志发现publishEvent是同步执行的在事务提交前就发了短信。如果事务回滚了比如库存扣减失败短信已经发出去了——用户收到了下单成功但订单根本没创建。我去掉publishEvent直接在事务提交后调smsService。看起来解决了。然后产品说下单成功还要发 App Push、记录用户行为、更新推荐算法。我写了三个调用。又过一周产品说VIP 用户要多发一封邮件。我又加一个。到此为止OrderService.createOrder() 里有 5 个非核心的副作用调用每个都可能抛异常阻塞主流程。这就是所谓的观察者模式缺位导致的代码腐化。观察者模式的正确姿势不是你写个 Listener 就叫观察者了很多 Java 工程师觉得我用了 EventListener 就是用了观察者模式但实际情况是 90% 的人都在误用。观察者模式的核心契约是这个Subject 不应该知道 Observer 是谁Observer 也不应该影响 Subject 的主流程。对照这两个标准上面那个例子两样都违反了 - OrderService 直接调用 smsService知道 Observer 是谁 - Observer 的异常会阻断下单主流程Spring 的TransactionalEventListener就是为这个场景设计的java Service public class OrderService {Autowired private ApplicationEventPublisher publisher; Transactional public void createOrder(OrderDTO dto) { Order order orderRepo.save(dto.toEntity()); publisher.publishEvent(new OrderCreatedEvent(order)); // 代码到此为止。其他副作用由 Listener 负责 }}Component public class OrderNotificationListener {TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { // 事务提交后才执行不会在回滚时误发 smsService.send(event.getOrder().getUserPhone(), 下单成功); }}Component public class OrderAnalyticsListener {TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) Async // 异步执行不阻塞主流程 public void onOrderCreated(OrderCreatedEvent event) { analyticsService.record(order_created, event.getOrder().getId()); }} TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT)保证了 Listener 只在事务成功提交后执行。如果事务回滚Listener 根本不会被触发。加上Async分析埋点这类非关键操作就不会拖慢下单接口的响应时间。你以为这就完了生产环境会教做人的坑一AFTER_COMMIT 不是 AFTER_COMPLETIONjava // 事务提交成功 → 执行 TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT)// 事务结束无论提交还是回滚→ 都会执行 TransactionalEventListener(phase TransactionPhase.AFTER_COMPLETION) 大多数业务通知应该用 AFTER_COMMIT——只有真正写库成功了才发。但如果你要在回滚时发一个下单失败的消息就得用 AFTER_COMPLETION 配合判断事务状态。另外AFTER_COMMIT 的 Listener 如果自己抛了异常不会回滚主事务——因为主事务已经提交了。也就是说发短信失败不会让订单回滚。这是你想要的吗不一定。如果你的业务要求短信发不出去订单就不能算完成那 AFTER_COMMIT 就不适合——你得回到事务内同步调用。坑二Async 的线程池不隔离慢任务拖死整个系统Spring 默认的Async线程池是SimpleAsyncTaskExecutor——这玩意每次创建一个新线程没有上限。高并发下直接 OOM。你必须自己配置线程池java Configuration EnableAsync public class AsyncConfig {Bean(eventExecutor) public Executor eventExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(20); executor.setQueueCapacity(100); executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy() // 满了让调用线程执行 ); executor.setThreadNamePrefix(event-); executor.initialize(); return executor; }}// Listener 指定线程池 Async(eventExecutor) TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { // ... } 这里还有一个容易被忽略的细节CallerRunsPolicy——当队列满了任务由发布事件的线程也就是你的 HTTP 线程自己执行。这听起来会让接口变慢但比直接丢弃任务导致数据丢失要好。取舍在于你的业务对延迟的容忍度。坑三异步事件里拿不到 HttpServletRequest这应该是踩坑率最高的问题了java // 异步事件监听器 Async TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) public void onOrderCreated(OrderCreatedEvent event) { // ❌ 异步线程里这玩意儿是 null HttpServletRequest request ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); }因为Async在新线程执行RequestContextHolder 是基于 ThreadLocal 的新线程里没有。解决方案在事件里携带需要的上下文而不是在 Listener 里现取。java public class OrderCreatedEvent { private final Order order; private final String clientIp; // 从 request 里取出放到事件里 private final String userAgent;public OrderCreatedEvent(Order order, HttpServletRequest request) { this.order order; this.clientIp request.getRemoteAddr(); this.userAgent request.getHeader(User-Agent); }} 事件的职责不只是通知还应该携带 Observer 需要的全部数据。坑四事件顺序没有保证EventListener和TransactionalEventListener的多个 Listener 之间执行顺序是不确定的。如果 A Listener 必须比 B Listener 先执行比如先更新缓存再发消息你得用Order注解java TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) Order(1) public void updateCache(OrderCreatedEvent event) { ... }TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT) Order(2) public void notifyMq(OrderCreatedEvent event) { ... } 但如果有多层依赖关系Order就会变得脆弱。更好的方案是用消息队列替代 Spring Event 做跨服务的事件传递。Spring Event 适合进程内的轻量级解耦跨服务的事件驱动还是交给 MQ 更靠谱。什么时候不该用观察者模式Spring Event 好用到容易滥用。有几个场景应该克制场景一需要强一致性的操作。比如创建订单 扣库存——这不适合用事件解耦因为扣库存失败必须回滚订单。这类操作应该在同一事务内完成。场景二事件数量爆炸。一个操作发布 20 个事件每个事件有 3 个 Listener你根本追踪不到整个调用链。与其用事件满天飞不如回归到明确的流程编排中介者模式/编排器。场景三只有两个组件通信。如果 A 只需要通知 B直接调用比绕一层事件更清晰。观察者模式的价值在 Observer 数量 ≥ 3 时才真正体现出来。实际经验总结Spring Event 机制是观察者模式的工程化实现但它不是银弹。正确用法TransactionalEventListener(AFTER_COMMIT) 自定义线程池 事件携带完整上下文 Order控制顺序。但一旦你发现自己在写第 10 个EventListener就该停下来想一想了——你是不是在用事件机制逃避架构设计把正常的流程编排拆成一堆离散的 Listener除了让调用链不可追踪之外没有任何好处。观察者模式解决的是Subject 不依赖 Observer的问题不是我不知道自己代码在干嘛的问题。我正在做一个小程序叫「爪爪代码冒险记」用卡皮巴拉的漫画故事讲 23 个设计模式观察者模式这一集是森林广播站的故事——猫头鹰当 Subject 发布消息动物们各自订阅自己关心的内容。感兴趣的可以搜一下或者等我后面的文章每个模式我都会同步对应的小程序内容。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

原生JS实现月下写真页:懒加载、视差滚动与动效性能优化 2026/9/3 2:19:22

原生JS实现月下写真页:懒加载、视差滚动与动效性能优化

月下主题的动态写真页面,很多第一次做的人会把它当成“把图片放在深色背景上,再加一点透明渐变”。实际动手时却发现,图片明明加载了,但滚动起来很生硬;月亮光晕一放大就出现色块;图片进入视口时动画要么不…

阅读更多 →
宝可梦二创剪辑:如何把角色登场剪出悬念与节奏 2026/9/3 2:19:22

宝可梦二创剪辑:如何把角色登场剪出悬念与节奏

最近在整理宝可梦二创剪辑素材时,看到一个标题:劈斧螳螂行踪现身,新角色莎丽登场!。这几个词放在一起,其实已经是一段短片脚本的雏形。很多拿到这类素材的人会立刻开始堆镜头,结果剪出来的片子像 PPT 轮播&…

阅读更多 →
STM32F103硬件I2C驱动DS3231高精度RTC完整方案 2026/9/3 2:19:22

STM32F103硬件I2C驱动DS3231高精度RTC完整方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
熙彼儿WS200Pro评测:百元档LDAC降噪耳机的性价比之选 2026/9/3 2:19:22

熙彼儿WS200Pro评测:百元档LDAC降噪耳机的性价比之选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Unity Mesh特效实战:从角色贴合到性能优化的完整指南 2026/9/3 2:19:22

Unity Mesh特效实战:从角色贴合到性能优化的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Pandas读写CSV/TXT全攻略:read_csv与to_csv参数详解 2026/9/3 2:16:22

Pandas读写CSV/TXT全攻略:read_csv与to_csv参数详解

写了一个多月的 CSV 实战脚本,回头发现自己对pd.read_csv()的理解其实只停留在“能跑就行”。直到某天接手一份带时间、带单位、还带着乱码的中文业务表,才被sep、encoding、dtype、parse_dates这些参数挨个教做人。这篇文章就把 Pandas 读写 txt 和 csv…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞