网上被吹爆的Spring Event事件订阅有缺陷,不要用

Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。

之前我已经被Spring Event(事件发布订阅组件)坑过一次。那次是在服务关闭期间,有请求未处理完成,当调用Spring Event时,出现异常。

根源是:Spring关闭期间,不得调用GetBean,也就是无法使用Spring Event 。详情点击这里查看

然而新项目大量使用了Spring Event,在另一个Task服务还未来得及移除Spring Event的情况下,出现了类似的问题。

当领导听说新引入的Spring Event再次出现问题时,非常愤怒,因为一个月内出现了两次故障。在复盘会议上,差点爆粗口。

在上线过程中,丢消息了?

“五哥,你看一眼钉钉给你发的监控截图,线上好像有丢消息?” 旁边同事急匆匆的跟我说。

“线上有问题?强哥在上线,我让他先暂停下~”,于是我赶紧通知强哥,先暂停发布。优先排查线上问题~

怎么会有问题呢?我有点意外,正好我和强哥各有代码上线,我只改动很小一段代码。我对这次代码变更很自信,坚信不会有问题,所以我并没有慌乱和紧张。搁之前,我的小心脏早就怦怦跳了!

诡异的情况

出现问题的业务逻辑是 消费A 消息,经过业务处理后,再发送B消息。

623e878d73734f0defe547a608783841.jpeg 从线上监控和日志分析,Task服务收到了 A 消息,然后处理失败了。诡异之处是没有任何异常日志和异常打点,仿佛凭空消失了。

分析代码分支后,我和同事十分确信,任何异常退出的代码分支都有打印异常日志和上报异常监控打点,出现异常不可能不留一丝痕迹。

正当陷入困境之时,我们发现蹊跷之处。“丢消息”的时间只有 3秒钟,之后便恢复正常。问题出在启动阶段,消息A进入Task服务,服务还未完全发布完成时,导致不可预测的情况发生。

当分析Spring 源代码以后,我们发现原因出在 Spring Event……

在详细说明问题根源前,我简单介绍一下 SpringEvent使用,熟悉它的读者,可以自行跳过。

Spring Event的简单使用

声明事件

自定义事件需要继承Spring ApplicationEvent。我选择使用泛型,支持子类可以灵活关联事件的内容。

e4a56c1e4b8dbfc76789bc90650ca89b.jpeg

发布事件

使用Spring上下文 ApplicationContext发布事件

89f677241c88228c516bdf35308e0838.jpeg

监听事件

监听器只需要 在方法上声明为 EventListener注解,Spring就会自动找到对应的监听器。Spring会根据方法入参的事件类型和 发布的事件类型 自动匹配。

5983b55cd26827522ebfa63b4172b11f.jpeg

服务启动阶段,Spring Event 注册严重滞后

在Kafka 消费逻辑中,通过Spring Event发布事件,业务逻辑都封装在 Event Listenr 中。经过分析和验证后,我们终于发现问题所在。

当Kafka 消费者已经开始消费消息,但Spring Event 监听者还没有注册到Spring ApplicationContext中, 所以Spring Event 事件发布后,没有Event Listener消费该事件。3秒钟以后,Event Listener被注册到Spring后,异常就消失了。

问题根源在:Event Listener 注册的时间点滞后于 init-method 的时间点!

88348c916be5edd50358b254e625ccad.jpeg

init-method ——— Kafka 开始监听的时间点

Kafka 消费者的启动点 在 Spring init-method中,例如下面的 XML中,init-method 声明 HelloConsumer 的初始化方法为 init方法。在该方法中注册到Kafka中,抢占分片,开始消费消息。

<bean id="kafkaConsumer" class="com.helloworld.KafkaConsumer" init-method="init" destroy-method="destroy">

如果在init-method 方法中,成功注册到Kafka,抢占到分片,然而 Spring Event Listener还未注册到Spring ,就会 “Spring事件丢失” 的现象。

EventListener注册到Spring 的时间点

在Spring的启动过程中,EventListener 的启动点滞后于 init-method 。如下图Spring的启动顺序所示。

其中init-method在InitializingBean中被触发,而 EventListener 在 SmartInitializingSingleton 中初始化。由于启动顺序的先后关系,当init-method的执行时间较长时(例如连接超时),就会出现Kafka已开始消费,但EventListener还未注册的问题。

Spring 启动顺序 6ba563bc26fa61dd297f6189200cb123.jpeg

InitializingBean 的初始化代码

通过分析 Spring源代码。InitializingBean 阶段, invokeInitMethod 会执行init-method方法,Kafka消费者就是在init-method 执行完成后开始消费kafka消息。 8f5468fe559a88d558f6244393eea68b.jpeg

SmartInitializingSingleton

继续分析Spring源代码。 EventListenerMethodProcessor 是 SmartInitializingSingleton 子类,该类负责解析Spring 中所有的Bean,如果有方法添加EventListener注解,则将 EventListener方法 注册到 Spring 中

以下是代码截图 4cd1581f825c69d7d53ea770adeb130c.jpeg

Spring Event很好,我劝你别用

通过代码分析可以发现,在Spring中,init-method方法会先执行,然后才会解析和注册Event Listener。因此,在消费Kafka和注册EventListener之间存在一个时间间隔,如果在这期间发布了Spring Event,该事件将无法被消费。

通常情况下,这个时间间隔非常短暂,但是当init-method执行较慢时,比如Kafka消费者 A 初始化很快,但是Kafka消费者 B 建立连接超时导致init-method执行时间较长,就会出现问题。在这段时间内,Kafka消费者 A 发布的Spring事件无法被消费。

尽管这不是一个稳定必现的问题,但是当线上流量较大时,它发生的概率会增加,后果也会更严重。我们在上线3个月后,线上环境才首次遇到这个问题。

在《服务关闭期,Spring Event消费失败》这篇文章中,有读者评论提到了这个问题。

f520434b72a68542a866444142d7155a.jpeg

有朋友说: 这和spring event有什么关系,自己实现一套,不也有同样的问题吗?关键是得优雅停机啊!

他所说的是正确的,如果服务能够完美地进行优雅发布,即使是在大流量场景下,Spring Event也不会出现问题。

然而,要确保服务的优雅发布是非常困难的,尤其是公司的项目,很多框架和组件都是基础架构组提供的,与其费力去改造公司的项目,不如不使用Spring Event。

一般情况下,公司的项目通常会在 init-method 方法中,统一初始化消息队列 MQ 消费者。如果想要安全地使用Spring Event,必须等到Spring完全发布完成之后才能初始化 Kafka 消费者。与其按照Spring Event的要求改造公司的项目,还不如不使用Spring Event。

对于公司的项目来说,稳定性是最重要的。

与其冒着巨大的风险使用Spring Event,不如自己手写事件监听模式,这样反而简易、安全、可靠。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/151840.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# params关键字

在C#中&#xff0c;params关键字用于指定一个方法参数&#xff0c;它可以接受任意数量的参数&#xff0c;或者说是一个参数数组。当使用params关键字时&#xff0c;你可以向方法传递逗号分隔的参数列表&#xff0c;或者是一个数组。在方法内部&#xff0c;这些参数被处理为一个…

ARM CoreLink CCN 互连总线介绍

NIC NOC CCI CMN CNN NI cmn-700 nic-700 ni-700 MLGB这都是啥玩意? 后期博文或视频将会更新这一系列。今天先温习一下CNN的概念,这是来自2014年的文章,然后稍微整理总结一番。 以下是正文… 现代主流和优质 ARM 片上系统 (SoC) 产品使用 CoreLink 缓存一致性网络 (CCN) 504…

SpringBoot-过滤器Filter+JWT令牌实现登录验证

登录校验-Filter 分析 过滤器Filter的快速入门以及使用细节我们已经介绍完了&#xff0c;接下来最后一步&#xff0c;我们需要使用过滤器Filter来完成案例当中的登录校验功能。 我们先来回顾下前面分析过的登录校验的基本流程&#xff1a; 要进入到后台管理系统&#xff0c;我…

java 位运算 表示状态小记

单一状态码var i2 1 << 2; //4 左移var i3 1 << 3; //8 左移var i4 1 << 4; //16 左移var i5 1 << 5; //32 左移数据状态值var huo i3 | i4 | i2; //28 或//判断状态System.out.println("yu1" (huo & i2)); // 4 & 与System.o…

CentOS 安装etcd集群 —— 筑梦之路

环境说明 192.168.1.11 192.168.1.12 192.168.1.13 yum在线安装 yum install etcd -y #etcd01 cat > /etc/etcd/etcd.conf <<EOF ETCD_NAMEetcd01 ETCD_LISTEN_PEER_URLS"http://0.0.0.0:2380" ETCD_LISTEN_CLIENT_URLS"http://0.0.0.0:2379&quo…

IDEA运行thymeleaf的html文件打开端口为63342且连不上数据库

这边贴apple.html代码 <!DOCTYPE html> <html xmlns:th"http://www.thymeleaf.org"> <head><meta charset"UTF-8"><title>User List</title> </head> <body> <h1>User List</h1> <table&…

elementui表格自定义指令控制显示哪些列可以拖动

Vue.directive(tableBorder, function (el, {value}) {// value允许传字符串数字和数组el.classList.add(z_table_hasBorder)let hasStyle el.querySelector(style)if(hasStyle){hasStyle.remove()}let style document.createElement(style)let str .z_table_hasBorder .el…

线上AB实验的日志分析

1&#xff0c;如果策略只影响部分商家/商品&#xff0c;则统计分析出受影响这部分的效果指标。 具体操作&#xff1a;在实验组圈选出 受策略影响的商家/商品&#xff0c;对这部分商家/商品&#xff0c;统计实验组VS对照组的效果指标。 分析出了 受影响这部分商家/商品的正向或…

【数据结构与算法】Kadane‘s算法(动态规划、最大子数组和)

文章目录 一、算法原理二、例题2.1 最大子数组和2.2 环形子数组的最大和 一、算法原理 Kadanes算法是一种用于解决最大子数组和问题的动态规划算法。这类问题的目标是在给定整数数组中找到一个连续的子数组&#xff0c;使其元素之和最大&#xff08;数组含有负数&#xff09;。…

【Linux 内核分析课程作业 1】mmap 实现一个 key-valueMap

作业一 功能要求利用 mmap(虚拟内存映射文件) 机制实现一个带持久化能力的 key-valueMap 系统&#xff0c;至少支持单机单进程访问。(可能用到的 linux API: mmap、msync、mremap、munmap、ftruncate、fallocate 等) 电子版提交方式&#xff1a; 2023 年 11 月 20 日 18:00 前通…

IObit Unlocker丨解除占用程序软件

更多内容请收藏&#xff1a;https://rwx.tza-3.xyz 官网&#xff1a;IObit Unlocker “永远不用担心电脑上无法删除的文件。” 界面简单&#xff0c;支持简体中文&#xff0c;一看就会&#xff0c;只需要把无法删除/移动的文件或整个U盘拖到框里就行。 解锁率很高&#xff0c;…

Unity热更新资源和代码-(学前必读)

系列文章目录 在学习完整的热更新流程之前&#xff0c;需要对lua、xlua、AssetBundle有一定的了解&#xff0c;没有基础的同学&#xff0c;可以看之前的博客&#xff0c;有一定的了解后再学习就会事半功倍了 Lua入门课程 AssetBundle入门 xLua入门 前言 在对热更新有初步了解…

Java爬虫的使用案例及简单总结

通过三个简单的案例,来实现的,都是不加验证的情况下. 如果有拼图验证网上也有对应的实现方法自行查找即可. 这里仅仅是一个简单的Demo, 练习使用 0. 爬取网站的配置: article:config:#中央新闻网-三农头条数据部分ntvUrl: https://www.ntv.cn/# 全国农技推广网- 农技动态部分n…

elasticsearch 概述

初识elasticsearch 了解ES elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大功能&#xff0c;可以帮助我们从海量数据中快速找到需要的内容 例如&#xff1a; 在GitHub搜索代码 在电商网站搜索商品 ELK技术栈 elasticsearc…

简述如何使用Androidstudio对文件进行保存和获取文件中的数据

在 Android Studio 中&#xff0c;可以使用以下方法对文件进行保存和获取文件中的数据&#xff1a; 保存文件&#xff1a; 创建一个 File 对象&#xff0c;指定要保存的文件路径和文件名。使用 FileOutputStream 类创建一个文件输出流对象。将需要保存的数据写入文件输出流中…

Vue3的异步组件使用

通过defineAsyncComponent引入组件&#xff0c;使用Suspense渲染 Suspense有两个插槽&#xff0c;一个default一个fallback <template><Suspense><template #default><userCard></userCard></template><template #fallback>加载中…

我叫:插入排序【JAVA】

1.自我介绍 插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置&#xff0c;以达到排序的目的。 2.继承我的思想 插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包…

WPF ObservableCollection 和 BindingList 有什么区别

ObservableCollection<T>和BindingList<T>都是.NET Framework中的泛型集合类型&#xff0c;它们都支持数据绑定和元素的增加、删除、修改等操作。然而&#xff0c;它们之间存在一些关键的差异&#xff0c;使它们在不同的场景下更有用。 ObservableCollection: Ob…

C#的MessagePack(unity)--02

高级API (MessagePackSerializer) MessagePackSerializer类是MessagePack for C# 的入口点。静态方法构成了MessagePack for C# 的主要API。 APIDescriptionSerialize<T>将一个对象图序列化为MessagePack二进制块。可以使用异步变体获取Stream。也有非泛型重载可用。De…

设计模式—结构型模式之外观模式(门面模式)

设计模式—结构型模式之外观模式&#xff08;门面模式&#xff09; 外观&#xff08;Facade&#xff09;模式又叫作门面模式&#xff0c;是一种通过为多个复杂的子系统提供一个一致的接口&#xff0c;而使这些子系统更加容易被访问的模式。 例子 我们的电脑会有很多 组件&am…