Springboot程序启动慢及JVM上的随机数与熵池策略-/dev/random
问题描述
线上环境中很容易出现一个java应用启动非常耗时的情况,在日志中可以发现是session引起的随机数问题导致的
o.a.c.util.SessionIdGeneratorBase : Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [170,241] milliseconds.
- 1.
分析
在Springboot程序中有内置的tomcat,在tomcat给的优化文档中,有一项是关于随机数生成时,采用的“熵源”(entropy source)的策略。
他提到tomcat7的session id的生成主要通过java.security.SecureRandom生成随机数来实现,随机数算法使用的是”SHA1PRNG”
private String secureRandomAlgorithm = "SHA1PRNG";
- 1.
在sun/oracle的jdk里,这个算法的提供者在底层依赖到操作系统提供的随机数据,在linux上,与之相关的是/dev/random和/dev/urandom。区别为:
/dev/random是阻塞的发生器
在读取时,/dev/random设备会返回小于熵池噪声总数的随机字节。/dev/random可生成高随机性的公钥或一次性密码本。若熵池空了,对/dev/random的读操作将会被阻塞,直到收集到了足够的环境噪声为止
而 /dev/urandom 则是一个非阻塞的发生器:
dev/random的一个副本是/dev/urandom (”unlocked”,非阻塞的随机数发生器),它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于/dev/random的。它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。
这也并不是说明/dev/urandom不是做高强度的伪随机数生成器,这个讨论可以看这个讨论:/dev/urandom 不得不说的故事
解决方法
方法一
在 jre/lib/security/java.security 这个文件里面把
securerandom.source=file:/dev/random
改为
securerandom.source=file:/dev/./urandom
方法二
在启动参数中添加以下系统属性
-Djava.security.egd=file:/dev/./urandom
- 1.
这个系统属性egd表示熵收集守护进程(entropy gathering daemon),但这里值为何要在dev和random之间加一个点呢?是因为一个jdk的bug,在这个bug的连接里有人反馈及时对 securerandom.source 设置为 /dev/urandom 它也仍然使用的 /dev/random,有人提供了变通的解决方法,其中一个变通的做法是对securerandom.source设置为 /dev/./urandom 才行
多说一嘴
在Docker中如何添加系统参数呢
首先在build镜像时 要使用ENTRYPOINT 举个例子
FROM jdk:alpine-security8
WORKDIR /
#解决中文乱码问题
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
ADD sms-server.jar sms-server.jar
ADD application.properties application.properties
ADD bootstrap.properties bootstrap.properties
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
##使用如下的命令 添加-e JAVA_OPT是不起作用的
#ENTRYPOINT ["java","-jar","sms-server.jar"]
##要使用这条命令才行
ENTRYPOINT java ${JAVA_OPTS} -jar sms-server.jar
然后在启动命令中添加对应的-e参数就可以了 举个例子
docker run --name sms-server-security \
## 如下即可
-e JAVA_OPTS='-Djava.security.egd=file:/dev/./urandom' \
-e spring.cloud.nacos.discovery.server-addr=192.169.1.82:8848 \
-e spring.cloud.nacos.config.server-addr=192.169.1.82:8848 \
-e spring.cloud.nacos.config.ext-config[0].data-id=sms-server-node1.properties \
-p 8070:8090 \
-v /opt/sms_server/log:/log \
-v /opt/sms_server/nacos:/root/nacos \
-d \
3a1c93c34756
测试对Spring启动原理的理解程度
我举个例子,测试一下,你对Spring启动原理的理解程度。
-
Rpc框架和Spring的集成问题。Rpc框架何时注册暴露服务,在哪个Spring扩展点注册呢?init-method 中行不行?
-
MQ 消费组和Spring的集成问题。MQ消费者何时开始消费,在哪个Spring扩展点”注册“自己?init-method 中行不行?
-
SpringBoot 集成Tomcat问题。如果出现已开启Http流量,Spring还未启动完成,怎么办?Tomcat何时开启端口,对外服务?
SpringBoot项目常见的流量入口无外乎 Rpc、Http、MQ 三种方式。一名合格的架构师必须精通服务的入口流量何时开启,如何正确开启?最近我遇到的两次线上故障都和Spring启动过程相关。(点击这里了解故障)
故障的具体表现是:Kafka消费组已经开始消费,已开启流量,然而Spring 还未启动完成。因为业务代码中使用的Spring Event事件订阅组件还未启动(订阅者还未注册到Spring),所以处理异常,出了线上故障。根本原因是————项目在错误的时机开启 MQ 流量,然而Spring还未启动完成,导致出现故障。
正确的做法是:项目在Spring启动完成后开启入口流量,然而我司的Kafka消费组 在Spring init-method bean 实例化阶段就开启了流量,导致故障发生。出现这样的问题,说明项目初期的程序员没有深入理解Spring的启动原理。
接下来,我再次抛出 11 个问题,说明这个问题————深入理解Spring启动原理的重要性。
- Spring还未完全启动,在 PostConstruct 中调用
getBeanByAnnotation
能否获得准确的结果? - 项目应该如何监听 Spring 的启动就绪事件?
- 项目如何监听Spring 刷新事件?
- Spring就绪事件和刷新事件的执行顺序和区别?
- Http 流量入口何时启动完成?
- 项目中在 init-method 方法中注册 Rpc 是否合理?什么是合理的时机?
- 项目中在 init-method 方法中注册 MQ 消费组是否合理? 什么是合理的时机?
- PostConstruct 中方法依赖ApplicationContextAware拿到 ApplicationContext,两者的顺序谁先谁后?是否会出现空指针!
- init-method、PostConstruct、afterPropertiesSet 三个方法的执行顺序?
- 有两个 Bean声明了初始化方法。 A使用 PostConstruct注解声明,B使用 init-method 声明。Spring一定先执行 A 的PostConstruct 方法吗?
- Spring 何时装配Autowire属性,PostConstruct 方法中引用 Autowired 字段什么场景会空指针?
精通Spring 启动原理,以上问题则迎刃而解。接下来,请大家和五哥,一起学习Spring的启动原理,看看Spring的扩展点分别在何时执行。
一起数数 Spring启动过程的扩展点有几个?
Spring的扩展点极多,这里为了讲清楚启动原理,所以只列举和启动过程有关的扩展点。
- BeanFactoryAware 可在Bean 中获取 BeanFactory 实例
- ApplicationContextAware 可在Bean 中获取 ApplicationContext 实例
- BeanNameAware 可以在Bean中得到它在IOC容器中的Bean的实例的名字。
- ApplicationListener 可监听 ContextRefreshedEvent等。
- CommandLineRunner 整个项目启动完毕后,自动执行
- SmartLifecycle#start 在Spring Bean实例化完成后,执行start 方法。
- 使用@PostConstruct注解,用于Bean实例初始化
- 实现InitializingBean接口,用于Bean实例初始化
- xml 中声明 init-method 方法,用于Bean实例初始化
- Configuration 配置类 通过@Bean注解 注册Bean到Spring
- BeanPostProcessor 在Bean的初始化前后,植入扩展点!
- BeanFactoryPostProcessor 在BeanFactory创建后植入 扩展点!
通过打印日志学习Spring的执行顺序
首先我们先通过 代码实验,验证一下以上扩展点的执行顺序。
- 声明
TestSpringOrder
分别继承以下接口,并且在接口方法实现中,日志打印该接口的名称。
public class TestSpringOrder implements
ApplicationContextAware,
BeanFactoryAware,
InitializingBean,
SmartLifecycle,
BeanNameAware,
ApplicationListener<ContextRefreshedEvent>,
CommandLineRunner,
SmartInitializingSingleton {
@Override
public void afterPropertiesSet() throws Exception {
log.error("启动顺序:afterPropertiesSet");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
log.error("启动顺序:setApplicationContext");
}
TestSpringOrder
使用 PostConstruct注解初始化,声明 init-method方法初始化。
@PostConstruct
public void postConstruct() {
log.error("启动顺序:post-construct");
}
public void initMethod() {
log.error("启动顺序:init-method");
}
- 新建 TestSpringOrder2 继承
public class TestSpringOrder3 implements
BeanPostProcessor,
BeanFactoryPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
log.error("启动顺序:BeanPostProcessor postProcessBeforeInitialization beanName:{}", beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.error("启动顺序:BeanPostProcessor postProcessAfterInitialization beanName:{}", beanName);
return bean;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
log.error("启动顺序:BeanFactoryPostProcessor postProcessBeanFactory ");
}
}
执行以上代码后,可以在日志中看到启动顺序!
实际的执行顺序
2023-11-25 18:10:53,748 [main] ERROR (TestSpringOrder3:37) - 启动顺序:BeanFactoryPostProcessor postProcessBeanFactory
2023-11-25 18:10:59,299 [main] ERROR (TestSpringOrder:53) - 启动顺序:构造函数 TestSpringOrder
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:127) - 启动顺序: Autowired
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:129) - 启动顺序:setBeanName
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:111) - 启动顺序:setBeanFactory
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:121) - 启动顺序:setApplicationContext
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder3:25) - 启动顺序:BeanPostProcessor postProcessBeforeInitialization beanName:testSpringOrder
2023-11-25 18:10:59,316 [main] ERROR (TestSpringOrder:63) - 启动顺序:post-construct
2023-11-25 18:10:59,317 [main] ERROR (TestSpringOrder:116) - 启动顺序:afterPropertiesSet
2023-11-25 18:10:59,317 [main] ERROR (TestSpringOrder:46) - 启动顺序:init-method
2023-11-25 18:10:59,320 [main] ERROR (TestSpringOrder3:31) - 启动顺序:BeanPostProcessor postProcessAfterInitialization beanName:testSpringOrder
2023-11-25 18:17:21,563 [main] ERROR (SpringOrderConfiguartion:21) - 启动顺序: @Bean 注解方法执行
2023-11-25 18:17:21,668 [main] ERROR (TestSpringOrder:58) - 启动顺序:SmartInitializingSingleton
2023-11-25 18:17:21,675 [main] ERROR (TestSpringOrder:74) - 启动顺序:start
2023-11-25 18:17:23,508 [main] ERROR (TestSpringOrder:68) - 启动顺序:ContextRefreshedEvent
2023-11-25 18:17:23,574 [main] ERROR (TestSpringOrder:79) - 启动顺序:CommandLineRunner
我通过在以上扩展点 添加 debug 断点,调试代码,整理出 Spring启动原理的 长图。过程省略…………
一张长图透彻解释 Spring启动顺序
实例化和初始化的区别
new TestSpringOrder(); new 创建对象实例,即为实例化一个对象;执行该Bean的 init-method 等方法 为初始化一个Bean。注意初始化和实例化的区别。
Spring 重要扩展点的启动顺序
-
BeanFactoryPostProcessor
BeanFactory初始化之后,所有的Bean定义已经被加载,但Bean实例还没被创建(不包括BeanFactoryPostProcessor
类型)。Spring IoC容器允许BeanFactoryPostProcessor读取配置元数据,修改bean的定义,Bean的属性值等。 -
实例化Bean
Spring 调用java反射API 实例化 Bean。 等同于 new TestSpringOrder();
-
Autowired 装配依赖
Autowired是 借助于 AutowiredAnnotationBeanPostProcessor 解析 Bean 的依赖,装配依赖。如果被依赖的Bean还未初始化,则先初始化 被依赖的Bean。在 Bean实例化完成后,Spring将首先装配Bean依赖的属性。
-
BeanNameAware setBeanName
-
BeanFactoryAware setBeanFactory
-
ApplicationContextAware setApplicationContext
在Bean实例化前,会率先设置Aware接口,例如 BeanNameAware BeanFactoryAware ApplicationContextAware 等
-
BeanPostProcessor postProcessBeforeInitialization
如果我想在 bean初始化方法前后要添加一些自己逻辑处理。可以提供 BeanPostProcessor接口实现类,然后注册到Spring IoC容器中。在此接口中,可以创建Bean的代理,甚至替换这个Bean。
-
PostConstruct 执行
接下来 Spring会依次调用 Bean实例初始化的 三大方法。
-
InitializingBean afterPropertiesSet
-
init-method 方法执行
-
BeanPostProcessor postProcessAfterInitialization
在 Spring 对Bean的初始化方法执行完成后,执行该方法
-
其他Bean 实例化和初始化
Spring 会循环初始化Bean。直至所有的单例Bean都完成初始化
-
所有单例Bean 初始化完成后
-
SmartInitializingSingleton Bean实例化后置处理
该接口的执行时机在 所有的单例Bean执行完成后。例如Spring 事件订阅机制的 EventListener注解,所有的订阅者 都是 在这个位置被注册进 Spring的。而在此之前,Spring Event订阅机制还未初始化完成。所以如果有 MQ、Rpc 入口流量在此之前开启,Spring Event就可能出问题!
所以强烈建议 Http、MQ、Rpc 入口流量在 SmartInitializingSingleton 之后开启流量。
Http、MQ、Rpc 入口流量必须在 SmartInitializingSingleton 之后开启流量。
- SmartLifecyle smart start 方法执行 Spring 提供的扩展点,在所有单例Bean的 EventListener等组件全部启动完成后,即Spring启动完成,则执行 start 方法。在这个位置适合开启入口流量!
Http、MQ、Rpc 入口流量适合 在 SmartLifecyle 中开启
-
发布 ContextRefreshedEvent 方法 该事件会执行多次,在 Spring Refresh 执行完成后,就会发布该事件!
-
注册和初始化 Spring MVC
SpringBoot 应用,在父级 Spring启动完成后,会尝试启动 内嵌式 tomcat容器。在此之前,SpringBoot会初始化 SpringMVC 和注册DispatcherServlet到Web容器。
-
Tomcat/Jetty 容器开启端口
SpringBoot 调用内嵌式容器,会开启并监听端口,此时Http流量就开启了。
-
应用启动完成后,执行 CommandLineRunner
SpringBoot 特有的机制,待所有的完全执行完成后,会执行该接口 run方法。值得一提的是,由于此时Http流量已经开启,如果此时进行本地缓存初始化、预热缓存等,稍微有些晚了! 在这个间隔期,可能缓存还未就绪!
所以预热缓存的时机应该发生在 入口流量开启之前,比较合适的机会是在 Bean初始化的阶段。虽然 在Bean初始化时 Spring尚未完成启动,但是调用 Bean预热缓存也是可以的。但是注意:不要在 Bean初始化时 使用 Spring Event,因为它还未完成初始化 。
回答 关于 Spring 启动原理的若干问题
- init-method、PostConstruct、afterPropertiesSet 三个方法的执行顺序。
回答: PostConstruct,afterPropertiesSet,init-method
- 有两个 Bean声明了初始化方法。 A使用 PostConstruct注解声明,B使用 init-method 声明。Spring一定先执行 A 的PostConstruct 方法吗?
回答:Spring 会循环初始化Bean实例,初始化完成1个Bean,再初始化下一个Bean。Spring并没有使用这种机制启动,即所有的Bean先执行 PostConstruct,再统一执行afterProperfiesSet。 此外,A、B两个Bean的初始化顺序不确定,谁先谁后不确定。无法保证 A 的PostConstruct 一定先执行。除非使用 Order注解,声明Bean的初始化顺序!
- Spring 何时装配Autowire属性,PostConstruct方法中引用 Autowired 字段是否会空指针?
Autowired装配依赖发生在 PostConstruct之前,不会出现空指针!
- PostConstruct 中方法依赖ApplicationContextAware拿到 ApplicationContext,两者的顺序谁先谁后?是否会出现空指针!
ApplicationContextAware 会先执行,不会出现空指针!但是当Autowired没有找到对应的依赖,并且声明了非强制依赖时,该字段会为空,有潜在 空指针风险。
- 项目应该如何监听 Spring 的启动就绪事件。
通过SmartLifecyle start方法,监听Spring就绪 。适合在此开启入口流量!
- 项目如何监听Spring 刷新事件。
监听 Spring Event ContextRefreshedEvent
- Spring就绪事件和刷新事件的执行顺序和区别。
Spring就绪事件会先于 刷新事件。两者都可能多次执行,要确保方法的幂等处理,避免重复注册问题
- Http 流量入口何时启动完成。
SpringBoot 最后阶段,启动完成Spring 上下文,才开启Http入口流量,此时 SmartLifecycle#start 已执行。所有单例Bean和SpringEvent等组件都已经就绪!
- 项目中在 init-method 方法中注册 Rpc是否合理?什么是合理的时机?
init 开启Rpc流量非常不合理。因为Spring尚未启动完成,包括 Spring Event尚未就绪!
- 项目中在 init-method 方法中注册 MQ消费组是否合理? 什么是合理的时机?
init 开启 MQ 流量非常不合理。因为Spring尚未启动完成,包括 Spring Event尚未就绪!
- Spring还未完全启动,在 PostConstruct 中调用 getBeanByAnnotation能否获得准确的结果?
虽然未启动完成,但是Spring执行该getBeanByAnnotation方法时,会率先检查 Bean定义,如果Bean定义对应的 Bean尚未初始化,则初始化这些Bean。所以即便是Spring初始化过程中调用,调用结果是准确的。
源码级别介绍
SmartInitializingSingleton 接口的执行位置
下图代码说明了,Spring在初始化全部 单例Bean以后,会执行 SmartInitializingSingleton 接口。
Autowired 何时装配Bean的依赖
在Bean实例化之后,但初始化之前,AutowiredAnnotationBeanPostProcessor
会注入Autowired字段。
SpringBoot 何时开启Http端口
下图代码中可以看到,SpringBoot会首先启动 Spring上下文,完成后才启动 嵌入式Web容器,初始化SpringMVC,监听端口
Spring 初始化Bean的关键代码
下图我加了注释,Spring初始化Bean的关键代码,全在 这个方法里,感兴趣的可以自行查阅代码 。 AbstractAutowireCapableBeanFactory#initializeBean
Spring CommandLineRunner 执行位置
Spring Boot外部,当启动完Spring上下文以后,最后才启动 CommandLineRunner。
总结
SpringBoot 会在Spring完全启动完成后,才开启Http流量。这给了我们启示:应该在Spring启动完成后开启入口流量。Rpc和 MQ流量 也应该如此,所以建议大家 在 SmartLifecype 或者 ContextRefreshedEvent 等位置 注册服务,开启流量。
例如 Spring Cloud Eureka
服务发现组件,就是在 SmartLifecype中注册服务的!
网上被吹爆的Spring Event事件订阅有缺陷,一个月内我被坑了两次!
Spring Event事件订阅框架,被网上一些人快吹上天了,然而我们在新项目中引入后发现,这个框架缺陷很多,玩玩可以,千万不要再公司项目中使用。还不如自己手写一个监听者设计模式,那样更稳定、可靠。
之前我已经被Spring Event(事件发布订阅组件)坑过一次。那次是在服务关闭期间,有请求未处理完成,当调用Spring Event时,出现异常。
根源是:Spring关闭期间,不得调用GetBean,也就是无法使用Spring Event 。详情点击这里查看
然而新项目大量使用了Spring Event
,在另一个Task服务还未来得及移除Spring Event
的情况下,出现了类似的问题。
当领导听说新引入的Spring Event
再次出现问题时,非常愤怒,因为一个月内出现了两次故障。在复盘会议上,差点爆粗口。
在上线过程中,丢消息了?
“五哥,你看一眼钉钉给你发的监控截图,线上好像有丢消息?” 旁边同事急匆匆的跟我说。
“线上有问题?强哥在上线,我让他先暂停下~”,于是我赶紧通知强哥,先暂停发布。优先排查线上问题~
怎么会有问题呢?我有点意外,正好我和强哥各有代码上线,我只改动很小一段代码。我对这次代码变更很自信,坚信不会有问题,所以我并没有慌乱和紧张。搁之前,我的小心脏早就怦怦跳了!
诡异的情况
出现问题的业务逻辑是 消费A 消息,经过业务处理后,再发送B消息。
从线上监控和日志分析,Task服务收到了 A 消息,然后处理失败了。诡异之处是没有任何异常日志和异常打点,仿佛凭空消失了。
分析代码分支后,我和同事十分确信,任何异常退出的代码分支都有打印异常日志和上报异常监控打点,出现异常不可能不留一丝痕迹。
正当陷入困境之时,我们发现蹊跷之处。“丢消息”的时间只有 3秒钟,之后便恢复正常。问题出在启动阶段,消息A进入Task服务,服务还未完全发布完成时,导致不可预测的情况发生。
当分析Spring 源代码以后,我们发现原因出在 Spring Event……
在详细说明问题根源前,我简单介绍一下 SpringEvent使用,熟悉它的读者,可以自行跳过。
Spring Event的简单使用
声明事件
自定义事件需要继承Spring ApplicationEvent。我选择使用泛型,支持子类可以灵活关联事件的内容。
public class BaseEvent<T> extends ApplicationEvent {
private final T data;
public BaseEvent(T source) {
super(source);
this.data = source;
}
public T getData() {
return data;
}
}
发布事件
使用Spring上下文 ApplicationContext发布事件
applicationContext.publishEvent(new BaseEvent<>(param));
Idea为Spring提供了跳转工具,点击绿色按钮位置,就可以 跳转到事件的监听器列表。
监听事件
监听器只需要 在方法上声明为 EventListener注解,Spring就会自动找到对应的监听器。Spring会根据方法入参的事件类型和 发布的事件类型 自动匹配。
@EventListener
public void handleEvent(BaseEvent<PerformParam> event) {
//消费事件
}
服务启动阶段,Spring Event
注册严重滞后
在Kafka 消费逻辑中,通过Spring Event发布事件,业务逻辑都封装在 Event Listenr 中。经过分析和验证后,我们终于发现问题所在。
当Kafka 消费者已经开始消费消息,但Spring Event 监听者还没有注册到Spring ApplicationContext中, 所以Spring Event 事件发布后,没有Event Listener消费该事件。3秒钟以后,Event Listener被注册到Spring后,异常就消失了。
问题根源在:Event Listener
注册的时间点滞后于 init-method
的时间点!
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 启动顺序
InitializingBean 的初始化代码
通过分析 Spring源代码。InitializingBean 阶段, invokeInitMethod 会执行init-method方法,Kafka消费者就是在init-method 执行完成后开始消费kafka消息。
SmartInitializingSingleton
继续分析Spring源代码。 EventListenerMethodProcessor
是 SmartInitializingSingleton
子类,该类负责解析Spring 中所有的Bean,如果有方法添加EventListener
注解,则将 EventListener
方法 注册到 Spring 中
以下是代码截图
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消费失败》这篇文章中,有读者评论提到了这个问题。
有朋友说: 这和spring event有什么关系,自己实现一套,不也有同样的问题吗?关键是得优雅停机啊!
他所说的是正确的,如果服务能够完美地进行优雅发布,即使是在大流量场景下,Spring Event也不会出现问题。
一般情况下,公司的项目通常会在 init-method 方法中,统一初始化消息队列 MQ 消费者。如果想要安全地使用Spring Event,必须等到Spring完全发布完成之后才能初始化 Kafka 消费者。
对于公司的项目来说,稳定性非常重要。引入 SpringEvent 前,一定要确保服务的入口流量在正确的时点开启。