专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
天津日报  ·  最新!这个岗位,急缺,高薪! ·  2 天前  
天津日报  ·  最新!这个岗位,急缺,高薪! ·  2 天前  
第1眼新闻  ·  微信又出新功能!这次关于抢红包🤩 ·  2 天前  
第1眼新闻  ·  微信又出新功能!这次关于抢红包🤩 ·  2 天前  
宁德广播电视台  ·  微信又上新功能!网友:太方便了 ·  2 天前  
西安头条  ·  被指吃相难看!爱奇艺回应 ·  2 天前  
51好读  ›  专栏  ›  Java基基

Spring Event 别瞎用!从我司的悲剧中,我总结了6条最佳实践

Java基基  · 公众号  ·  · 2025-02-08 11:55

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、 商城 、支付、工作流、大屏报表、微信公众号、 ERP CRM AI 大模型 等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本

来源:网络


今天我们重点聊聊使用 Spring Event 最为关键的几个问题。这是我司线上生产环境实际踩坑后,总结的极为宝贵的经验!

Spring Event框架实现了基于事件的发布订阅机制。开发者可以自定义事件,在某些业务场景发布事件,Spring 会将该事件广播给监听该事件的监听者。监听者可以实现Spring 的监听者接口 ApplicationListener 注册自己,也可以使用 EventListener 注解注册自己。

这是Spring Event 的简短介绍,网上有大量的入门级教程,我在此不过多赘述,进入正文!

1. 为什么说:业务系统一定要先实现优雅关闭服务,才能使用 Spring Event?

Spring  广播消息时,Spring会在 ApplicationContext 中查找所有的监听者,即需要 getBean 获取 bean 实例。然而 Spring 有个限制———— ApplicationContext 关闭期间,不得GetBean 否则会报错。

这个知识点得来不易。它是我们公司在线上环境发生故障后,最终定位的原因,大家一定要重视!

前几天,线上系统出现两条异常日志Get Bean时找不到对应的bean,调用堆栈让我非常迷惑,为什么Get Bean找不到对应的Bean呢? 如下图所示

堆栈中的信息 解释了原因。 Do not request a bean from a BeanFactory in a destroy method implementation

在应用上下文关闭时,不得从上下文中Get Bean。恰好,这个问题出现在服务关闭期间.....

由于系统流量较高,日订单几百万,即便在低峰期单机的并发度也是比较高的,所以服务在关闭期间有少量流量进来或未处理完。这个场景下,使用 Spring Event 发布事件,Spring 无法正常广播事件,一定会出现异常,导致处理失败!

大家一定要切记!使用 SpringEvent 之前,一定要先治理服务,确保服务关闭时,先切断入口流量(Http、MQ、RPC),然后再关闭服务,关闭 Spring 上下文!

详细的分析请参考:

https://juejin.cn/post/7281159113882468371

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

2. 为什么服务启动阶段,Spring Event 事件丢失了?

我们公司遇到的情况是, Kafka conumser 在 init-method 阶段开始消费,然而 Spring EventListener 被注册进 Spring 的时间点滞后于 init-method 时间点,所以 Kafka Consumer 中使用 Spring Event 发布事件时,没有找到监听者,出现消息处理丢失的情况。

从下图中可以看到 init-method 时间点 滞后于 EventListener 被注册的时间点。

简单来说:SpringBoot 会在Spring完全启动完成后,才开启Http流量。这给了我们启示:应该在Spring启动完成后开启入口流量。Rpc和 MQ流量 也应该如此,所以建议大家 在 SmartLifecype 或者 ContextRefreshedEvent 等位置 注册服务,开启流量。

最佳实践是:改造系统开启入口流量(Http、MQ、RPC)的时机,确保在Spring 启动完成后开启入口流量。

什么业务特点适合发布——订阅模式

每一个优秀的程序员都应该有自己的工具箱,他能在不同的业务场景选择最合适的工具。

SpringEvent 适合哪些业务场景呢?这由订阅发布模式的特性决定

  • 事件发布者并不关心事件如何被处理
  • 事件发布者不关心事件处理的结果
  • 事件订阅者有多个,可异步订阅,也可以同步订阅。
  • 事件订阅者之间各自独立,互不依赖。

发布订阅模式实现了发布和订阅两个模块的解耦。但是对于强一致性的场景,并不适合使用发布订阅模式。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

3. 强一致性场景不适合 订阅发布模式

强一致性的业务例如提单场景。提单阶段,库存扣减成功和订单提单成功务必完全一致。库存扣减失败但提单成功;提单失败,库存未回滚等场景都是要避免发生的异常场景!

提单场景,使用 Spring Event会有很多问题。假设提单前,发布提单前置事件,事件订阅者的业务逻辑可能有扣减库存,锁定优惠券资源等操作。库存扣减失败或者锁定资源失败需要回滚整个提单流程,然而 Spring  事件订阅模式无法提供这种 订阅异常——>回滚 的能力。

事件发布者无法获知哪些订阅消费失败,哪些订阅者成功?无法准确的触发回滚流程。(如果基于 Spring Event 强行搞回滚,也可以做到,但方案会很复杂!)

4. 最终一致性的业务特性适合——发布订阅模式

最终一致性场景非常适合使用 Spring Event。

例如提单成功后,发布 MQ ,释放锁等资源,可使用 SpringEvent 解耦。为什么呢?因为业务上确保提单成功后,提单实际上已经成功,后续的收尾工作不应该触发订单提单失败。

在提单成功事件的订阅者中,只有一种执行结果——————成功。即使出现失败,也应该重试直至成功。例如 发布 提单成功MQ 消息,释放提单锁等资源都是务必成功的业务逻辑。

再来举一个例子,我们公司在处理订单消息时使用了Spring Event框架。在这个场景中,我们需要处理履约完成、退款完成、订单过期等事件,并且每个事件都有一些独立的业务逻辑,每一个业务场景都属于最终一致性的场景。举个例子,履约完成后需要将履约数据和订单金额等数据通知结算系统。这个业务场景是最终一致性场景,而不是强一致性,这是因为通知结算即便失败,重试即可,无需回滚履约过程。

如果我们不使用Spring Event,那么我就需要手动编写观察者模式,并将订单消息根据状态通知到相应的观察者中。又或者每当新增一个业务逻辑时,我需要新增一个Kafka消费组,并且在代码中解析订单消息,然后根据状态将事件发送给相应的订阅者。总之我需要把事件按照状态分发给对应的监听者。

在这个场景中,使用Spring Event非常适合。可以将每个事件封装为Spring Event,并且每个业务逻辑都可以通过 @EventListener 注解来注册对应状态的事件监听器(不过需要注意的是,如果订阅者过多,那么Kafka消息的消费时间可能会增加。那么该如何解决呢?)。使用 Spring Event 框架比自己手写监听者模式强多了。

5. 使用SpringEvent 要有额外的可靠性保证!

Spring Event适用于需要保证最终一致性的业务场景,但为了确保可靠性,必须提供重试能力。通过使用 applicationContext.publishEvent(event) 方法发布事件,Spring会按顺序执行相关的订阅者。如果出现异常,publishEvent 方法会抛出异常,发布者能够感知订阅逻辑处理失败了。

在发布事件时,需要考虑事件订阅逻辑出现异常的情况,我提出三种解决办法

  1. 订阅者自行重试

订阅逻辑可自行重试保证成功。例如使用 Spring  retry注解可以保证出现异常时,重新执行该方法。

以下代码示例 performSuccess 方法抛出异常时,Spring 会重新执行该方法直至成功,最多重试 3 次,可设置间隔时间,重试间隔递增时间。

@Retryable(value = Exception.classmaxAttempts 3, backoff = @Backoff(delay = 100L, multiplier = 2))
public void performSuccess(PerformEvent event) {
    
}

使用 @Retryable 注解前,记得引入 spring-retry pom 依赖







请到「今天看啥」查看全文


推荐文章
天津日报  ·  最新!这个岗位,急缺,高薪!
2 天前
天津日报  ·  最新!这个岗位,急缺,高薪!
2 天前
宁德广播电视台  ·  微信又上新功能!网友:太方便了
2 天前
西安头条  ·  被指吃相难看!爱奇艺回应
2 天前
算法与数据结构  ·  轻松看懂机器学习十大常用算法
8 年前
玩转手机摄影  ·  妈妈,我也想养宠物!
7 年前
桌子的生活观  ·  讲真,我可能谈了场假恋爱
7 年前
中国经济学人  ·  上海楼市传出大消息!风向要变?
7 年前