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

今天,五阳哥,想重点聊聊使用 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 上下文!

详细的分析请参考 # 我真的很小心了,但还是被 SpringEvent 坑了!

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

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

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

我曾经整理过 Spring 的启动原理# 一张长图透彻理解SpringBoot 启动原理,这里面的关键结论是:

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

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

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

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

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

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

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

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 方法会抛出异常,发布者能够感知订阅逻辑处理失败了。

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

订阅者自行重试

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

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

Java 复制代码
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 100L, multiplier = 2))
public void performSuccess(PerformEvent event) {
    
}

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

xml 复制代码
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.4.RELEASE</version>
</dependency>

订阅者依赖 Kafka 消费组重试

如果在 Kafka 消费者中使用Spring Event,处理重试非常容易。只需要在消费异常时,向 Kafka 返回消费失败即可,Kafka 会自动进行重试。此外,还可以将消息发送到专门的死信队列,在死信队列中重新消费消息!不同公司的 Kafka 重试能力实现方案可能不同,大家自行选择。

主动上报故障异常到故障管理平台

# 聊一聊故障管理平台的建设](juejin.cn/post/728852...%E5%9C%A8%E8%BF%99%E7%AF%87%E6%96%87%E7%AB%A0%E4%B8%AD%EF%BC%8C%E6%88%91%E5%88%86%E6%9E%90%E4%BA%86%E4%BB%80%E4%B9%88%E6%98%AF%E6%95%85%E9%9A%9C%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0%E3%80%82%E6%80%BB%E4%BD%93%E6%80%9D%E6%83%B3%E6%98%AF%E4%B8%9A%E5%8A%A1%E7%B3%BB%E7%BB%9F%E4%B8%BB%E5%8A%A8%E4%B8%8A%E6%8A%A5%E6%95%85%E9%9A%9C%EF%BC%8C%E6%95%85%E9%9A%9C%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0%E7%BB%9F%E4%B8%80%E7%AE%A1%E6%8E%A7%E6%95%85%E9%9A%9C%EF%BC%8C%E6%8F%90%E4%BE%9B%E6%95%85%E9%9A%9C%E9%80%9A%E7%9F%A5%EF%BC%8C%E6%95%85%E9%9A%9C%E6%A3%80%E7%B4%A2%EF%BC%8C%E9%87%8D%E8%AF%95%E6%95%85%E9%9A%9C%E7%AD%89%E8%83%BD%E5%8A%9B%E3%80%82 "https://juejin.cn/post/7288527912882716735)%E5%9C%A8%E8%BF%99%E7%AF%87%E6%96%87%E7%AB%A0%E4%B8%AD%EF%BC%8C%E6%88%91%E5%88%86%E6%9E%90%E4%BA%86%E4%BB%80%E4%B9%88%E6%98%AF%E6%95%85%E9%9A%9C%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0%E3%80%82%E6%80%BB%E4%BD%93%E6%80%9D%E6%83%B3%E6%98%AF%E4%B8%9A%E5%8A%A1%E7%B3%BB%E7%BB%9F%E4%B8%BB%E5%8A%A8%E4%B8%8A%E6%8A%A5%E6%95%85%E9%9A%9C%EF%BC%8C%E6%95%85%E9%9A%9C%E7%AE%A1%E7%90%86%E5%B9%B3%E5%8F%B0%E7%BB%9F%E4%B8%80%E7%AE%A1%E6%8E%A7%E6%95%85%E9%9A%9C%EF%BC%8C%E6%8F%90%E4%BE%9B%E6%95%85%E9%9A%9C%E9%80%9A%E7%9F%A5%EF%BC%8C%E6%95%85%E9%9A%9C%E6%A3%80%E7%B4%A2%EF%BC%8C%E9%87%8D%E8%AF%95%E6%95%85%E9%9A%9C%E7%AD%89%E8%83%BD%E5%8A%9B%E3%80%82")

故障处理流程

当请求一直重试失败超过最大重试次数时,业务系统会上报到故障 MQ,故障管理平台消费 MQ,收集故障并落库。研发同学收到故障通知,介入排查问题。同时研发同学在故障管理后台可以看到故障列表、故障详情。 排查问题原因、敦促相关同事修复问题后,点击重试按钮。故障管理后台收到重试请求,会通过 Rpc SPI 调用到业务系统 重试故障,并告知管理后台成功和失败结果。

6. Spring 订阅者务必保证幂等

为了提高可靠性,要有额外的重试机制保证 Spring 订阅发布的可靠性。

有重试就要有幂等!要保证 订阅者逻辑具备幂等性。Spring 不知道哪些订阅者成功,哪些订阅者失败,下一次重试时,会全部执行所有的订阅者。所以订阅逻辑要做好幂等,防止数据不一致情况发生。

为什么有消息队列 MQ ,还需要 Spring Event

曾经有掘友给我评论,说我司对 Spring Event 的应用场景应该替换为 MQ。在此我解释一下

Spring Event和 MQ 都属于订阅发布模式的应用,然而 MQ 比 SpringEvent 强大且复杂。MQ 更适合应用之间的解耦、隔离、事件通知。例如订单支付、订单完成、订单履约完成等等事件需要广播出去,通知下游其他微服务, 这种场景更适合使用 MQ 。然而对于应用内需要订阅发布的场景更适合使用 SpringEvent。两者并不矛盾,MQ 能力更强大,技术方案也更"重"一些。Spring Event 更加小巧适合应用内订阅发布,实现业务逻辑解耦。

相关推荐
GraduationDesign23 分钟前
基于SpringBoot的在线文档管理系统的设计与实现
java·spring boot·后端
xiaosannihaiyl241 小时前
Scala语言的函数实现
开发语言·后端·golang
山山而川粤7 小时前
母婴用品系统|Java|SSM|JSP|
java·开发语言·后端·学习·mysql
玉红7779 小时前
R语言的数据类型
开发语言·后端·golang
lvbu_2024war0110 小时前
MATLAB语言的网络编程
开发语言·后端·golang
问道飞鱼10 小时前
【Springboot知识】Springboot进阶-实现CAS完整流程
java·spring boot·后端·cas
Q_192849990611 小时前
基于Spring Boot的电影网站系统
java·spring boot·后端
豌豆花下猫11 小时前
Python 潮流周刊#83:uv 的使用技巧(摘要)
后端·python·ai
凡人的AI工具箱11 小时前
每天40分玩转Django:Django部署概述
开发语言·数据库·后端·python·django
SomeB1oody11 小时前
【Rust自学】7.2. 路径(Path)Pt.1:相对路径、绝对路径与pub关键字
开发语言·后端·rust