英文:
How to prevent fail fast on external connection failure like Redis server in Spring boot?
问题
有没有办法防止spring-boot
应用程序在启动时因外部连接失败而失败?我已经找到了其他类似的问题,建议使用@Lazy
注解来防止@Configuration
bean的初始化,但是这个解决方案对我使用Spring Data Redis
和Jedis
客户端不起作用。
另外,其他解决方案,比如这个,是特定于应用程序中使用的依赖项。例如,Spring Cloud
有以下属性来控制故障快速行为 -
spring.cloud.config.fail-fast=true
您可以使用我为我的问题创建的这个项目来重现通过关闭redis
服务器来重现。
以下是我的代码示例 -
@Lazy
@Configuration
public class RedisConfiguration {
@Value("${spring.redis.sentinel.master}")
private String SENTINEL_MASTER;
@Value("${spring.redis.sentinel.nodes}")
private String SENTINEL_NODES;
@Value("${spring.redis.security.enabled:false}")
private boolean REDIS_SECURITY_ENABLED;
@Value("${spring.redis.security.password:}")
private String REDIS_PASSWORD;
@Lazy
@Bean // somehow this always gets initialized
public RedisConnectionFactory jedisConnectionFactory() {
// create set of sentinel nodes
System.out.println(SENTINEL_NODES);
Set<String> sentinelNodesSet = new HashSet<>(5);
StringTokenizer st = new StringTokenizer(SENTINEL_NODES, ",");
while (st.hasMoreTokens())
sentinelNodesSet.add(st.nextToken());
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(SENTINEL_MASTER, sentinelNodesSet);
if (REDIS_SECURITY_ENABLED) {
sentinelConfig.setPassword(REDIS_PASSWORD);
}
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(sentinelConfig);
return jedisConnectionFactory;
}
}
以下是异常跟踪 -
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'stringRedisTemplate' defined in class
path resource
[org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]:
Unsatisfied dependency expressed through method 'stringRedisTemplate'
parameter 0; nested exception is
org.springframework.beans.factory.BeanCreationException: Error
creating bean with name 'jedisConnectionFactory' defined in class path
resource [com/springboot/redisintegration/RedisConfiguration.class]:
Invocation of init method failed; nested exception is
redis.clients.jedis.exceptions.JedisConnectionException: All sentinels
down, cannot determine where is mysentinel master is running...
简而言之:
@Lazy
注解对RedisStandaloneConfiguration
有效,但对RedisSentinelConfiguration
无效,原因不明?- 使用
@Lazy
注解是有风险的,因为您需要确保使用Redis
的所有服务也是懒加载的。 - 寻找类似
spring.cloud.config.fail-fast=true
为Spring Cloud提供的解决方案。
更新:
我已经为此功能创建了以下Jira问题 -
https://jira.spring.io/browse/DATAREDIS-1208
英文:
Is there a way to prevent spring-boot
application from failing on startup due to external connection failures? I have found other similar questions that suggest using @Lazy
annotation to prevent @Configuration
beans initialisation but this solution did not work for me with Spring Data Redis
using Jedis
client.
Also, other solutions like this one are specific to dependencies being used in the app. For example, Spring Cloud
has below property to control failfast behaviour -
spring.cloud.config.fail-fast=true
You can use this project that I created for my problem to reproduce by shutting down the redis
server.
Below is how my code looks like -
@Lazy
@Configuration
public class RedisConfiguration {
@Value("${spring.redis.sentinel.master}")
private String SENTINEL_MASTER;
@Value("${spring.redis.sentinel.nodes}")
private String SENTINEL_NODES;
@Value("${spring.redis.security.enabled:false}")
private boolean REDIS_SECURITY_ENABLED;
@Value("${spring.redis.security.password:}")
private String REDIS_PASSWORD;
@Lazy
@Bean // somehow this always gets initialized
public RedisConnectionFactory jedisConnectionFactory() {
// create set of sentinel nodes
System.out.println(SENTINEL_NODES);
Set<String> sentinelNodesSet = new HashSet<>(5);
StringTokenizer st = new StringTokenizer(SENTINEL_NODES, ",");
while (st.hasMoreTokens())
sentinelNodesSet.add(st.nextToken());
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(SENTINEL_MASTER, sentinelNodesSet);
if (REDIS_SECURITY_ENABLED) {
sentinelConfig.setPassword(REDIS_PASSWORD);
}
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(sentinelConfig);
return jedisConnectionFactory;
}
Below is the exception trace -
> org.springframework.beans.factory.UnsatisfiedDependencyException:
> Error creating bean with name 'stringRedisTemplate' defined in class
> path resource
> [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]:
> Unsatisfied dependency expressed through method 'stringRedisTemplate'
> parameter 0; nested exception is
> org.springframework.beans.factory.BeanCreationException: Error
> creating bean with name 'jedisConnectionFactory' defined in class path
> resource [com/springboot/redisintegration/RedisConfiguration.class]:
> Invocation of init method failed; nested exception is
> redis.clients.jedis.exceptions.JedisConnectionException: All sentinels
> down, cannot determine where is mysentinel master is running... at
> org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:538)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1336)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1176)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
> ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
> ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
> ~[spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
> ~[spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
> ~[spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
> ~[spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
> ~[spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
> ~[spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
> ~[spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> com.springboot.redisintegration.RedisIntegrationApplication.main(RedisIntegrationApplication.java:21)
> ~[classes/:na] Caused by:
> org.springframework.beans.factory.BeanCreationException: Error
> creating bean with name 'jedisConnectionFactory' defined in class path
> resource [com/springboot/redisintegration/RedisConfiguration.class]:
> Invocation of init method failed; nested exception is
> redis.clients.jedis.exceptions.JedisConnectionException: All sentinels
> down, cannot determine where is mysentinel master is running... at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1794)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1307)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1227)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:884)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:788)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] ... 20 common frames
> omitted Caused by:
> redis.clients.jedis.exceptions.JedisConnectionException: All sentinels
> down, cannot determine where is mysentinel master is running... at
> redis.clients.jedis.JedisSentinelPool.initSentinels(JedisSentinelPool.java:249)
> ~[jedis-3.3.0.jar:na] at
> redis.clients.jedis.JedisSentinelPool.<init>(JedisSentinelPool.java:154)
> ~[jedis-3.3.0.jar:na] at
> redis.clients.jedis.JedisSentinelPool.<init>(JedisSentinelPool.java:122)
> ~[jedis-3.3.0.jar:na] at
> redis.clients.jedis.JedisSentinelPool.<init>(JedisSentinelPool.java:116)
> ~[jedis-3.3.0.jar:na] at
> org.springframework.data.redis.connection.jedis.JedisConnectionFactory.createRedisSentinelPool(JedisConnectionFactory.java:374)
> ~[spring-data-redis-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.data.redis.connection.jedis.JedisConnectionFactory.createPool(JedisConnectionFactory.java:358)
> ~[spring-data-redis-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.data.redis.connection.jedis.JedisConnectionFactory.afterPropertiesSet(JedisConnectionFactory.java:342)
> ~[spring-data-redis-2.3.3.RELEASE.jar:2.3.3.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1853)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] at
> org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1790)
> ~[spring-beans-5.2.8.RELEASE.jar:5.2.8.RELEASE] ... 31 common frames
> omitted
In short:
@Lazy
annotation works forRedisStandaloneConfiguration
but notRedisSentinelConfiguration
, not sure why?- Using
@Lazy
annotation is risky because you need to make sure all your services which are usingRedis
are loaded lazily too. - Looking for a solution like
spring.cloud.config.fail-fast=true
provided for spring cloud.
Update:
I've created below Jira issue for this feature -
答案1
得分: 1
Spring Cloud Config在底层使用了spring-retry
和AOP(spring-boot-starter-aop
)来配置重试机制。
这个过程是在ConfigServiceBootstrapConfiguration中实现的。
代码的相关部分如下:
/* @ConditionalOnProperty("spring.cloud.config.fail-fast") */
@ConditionalOnClass({ Retryable.class, Aspect.class, AopAutoConfiguration.class })
@Configuration(proxyBeanMethods = false)
@EnableRetry(proxyTargetClass = true)
@Import(AopAutoConfiguration.class)
@EnableConfigurationProperties(RetryProperties.class)
protected static class RetryConfiguration {
@Bean
@ConditionalOnMissingBean(name = "configServerRetryInterceptor")
public RetryOperationsInterceptor configServerRetryInterceptor(
RetryProperties properties) {
return RetryInterceptorBuilder.stateless()
.backOffOptions(properties.getInitialInterval(),
properties.getMultiplier(), properties.getMaxInterval())
.maxAttempts(properties.getMaxAttempts()).build();
}
}
正如你所看到的,基本思路是提供一个RetryConfiguration
,在考虑应用程序失败之前处理一定数量的重试。
Spring Cloud客户端的文档提供了有关配置此机制的不同属性的更多信息。你还可以在RetryProperties
类的源代码中查看默认值。
请尝试将两个必需的依赖项spring-retry
和spring-boot-starter-aop
包含在你的主配置的子配置中,为重试机制的配置属性提供一些合理的默认值,然后查看发生了什么。
你可以将解决方案推到极限,并尝试在非常多的情况下重新连接,也许增加它们之间的速度,等待服务器可用。
我认为@Lazy
注解将不再需要,并且可以安全地删除。
编辑
查看你的错误堆栈跟踪时,你还可以尝试禁用Spring Boot Redis自动配置类。
你可以在你的注解中这样做:
@SpringBootApplication(
exclude = { RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class }
)
或者在你的属性文件中:
spring.autoconfigure.exclude= \
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration, \
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
你还可以使用以下属性禁用Redis存储库配置:
spring.data.redis.repositories.enabled: false
一旦禁用了Redis自动配置,你就可以在适当的时候自由地实例化RedisTemplate
或与Redis交互所需的其他组件。
例如,你可以根据需要按需初始化它,尝试建立与Redis的连接,并在实际需要时初始化所有必需的工厂。你可以用try
和catch
包围初始化Redis连接的逻辑,并只有在有可用连接时才初始化RedisTemplate
,类似以下方式。
一方面:
public RedisConnectionFactory jedisConnectionFactory() {
try {
// create set of sentinel nodes
System.out.println(SENTINEL_NODES);
Set<String> sentinelNodesSet = new HashSet<>(5);
StringTokenizer st = new StringTokenizer(SENTINEL_NODES, ",");
while (st.hasMoreTokens())
sentinelNodesSet.add(st.nextToken());
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(SENTINEL_MASTER, sentinelNodesSet);
if (REDIS_SECURITY_ENABLED) {
sentinelConfig.setPassword(REDIS_PASSWORD);
}
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(sentinelConfig);
return jedisConnectionFactory;
} catch (redis.clients.jedis.exceptions.JedisConnectionException re) {
logger.error("Unable to initialize Redis connection factory", re);
return null;
}
}
另一方面:
public RedisTemplate getRedisTemplate() {
// 我们可以假设这两个方法都在同一个类中定义,尽管这不是必需的
final RedisConnectionFactory redisConnectionFactory = this.jedisConnectionFactory();
if (redisConnectionFactory == null) {
return null;
}
final RedisTemplate redisTemplate = new StringRedisTemplate(redisConnectionFactory);
return redisTemplate;
}
你可以根据需要以适当的方式使用这个RedisTemplate
,当然,根据需要进行缓存和重用。
这些方法可以在专门为此任务创建的服务或助手类中定义,并且当然不要在你的配置类中定义。
英文:
Under the hood Spring Cloud Config uses spring-retry
and AOP (spring-boot-starter-aop
) to configure a retry mechanism.
This process is implemented in ConfigServiceBootstrapConfiguration.
The relevant part of the code is this:
/* @ConditionalOnProperty("spring.cloud.config.fail-fast") */
@ConditionalOnClass({ Retryable.class, Aspect.class, AopAutoConfiguration.class })
@Configuration(proxyBeanMethods = false)
@EnableRetry(proxyTargetClass = true)
@Import(AopAutoConfiguration.class)
@EnableConfigurationProperties(RetryProperties.class)
protected static class RetryConfiguration {
@Bean
@ConditionalOnMissingBean(name = "configServerRetryInterceptor")
public RetryOperationsInterceptor configServerRetryInterceptor(
RetryProperties properties) {
return RetryInterceptorBuilder.stateless()
.backOffOptions(properties.getInitialInterval(),
properties.getMultiplier(), properties.getMaxInterval())
.maxAttempts(properties.getMaxAttempts()).build();
}
}
As you can see, the basic idea is to provide a RetryConfiguration
that handles a certain number of retries before considering the application failed.
The documentation of Spring Cloud Client provide more information about the different properties used for configuring this mechanism. You can see the default values also in the source code of the RetryProperties
class.
Please, try and include the two required dependencies, spring-retry
and spring-boot-starter-aop
, and the RetryConfiguration
as a child of your main configuration, provide some reasonable defaults for the configuration properties of the retry mechanism, and see what happens.
You can push the solution to the limit and try to reconnect on a very large number of occasions, perhaps increasing the cadence between them, waiting for the server to be available.
I think that the @Lazy
annotations will no be longer necessary and could be safely removed.
EDIT
Reviewing your error stack trace, one think you can also try is to disable String Boot Redis auto-configuration classes.
You can do it in your annotations:
@SpringBootApplication(
exclude = { RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class }
)
Or in your properties files:
spring.autoconfigure.exclude= \
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration, \
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
You can disable Redis repositories configuration with this property also:
spring.data.redis.repositories.enabled: false
Once you disable the Redis auto-configuration, you will be free to instantiate the RedisTemplate
or the stuff you need to interact with Redis when you consider appropriate.
You can, for instance, initialize it on demand, trying to establish a connection to Redis, by initializing all the required factories, when it is actually required. You can surround with try
and catch
the logic required to initialize your Redis connection and only initialize the RedisTemplate
if a connection is available, something like the following.
On one hand:
public RedisConnectionFactory jedisConnectionFactory() {
try {
// create set of sentinel nodes
System.out.println(SENTINEL_NODES);
Set<String> sentinelNodesSet = new HashSet<>(5);
StringTokenizer st = new StringTokenizer(SENTINEL_NODES, ",");
while (st.hasMoreTokens())
sentinelNodesSet.add(st.nextToken());
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(SENTINEL_MASTER, sentinelNodesSet);
if (REDIS_SECURITY_ENABLED) {
sentinelConfig.setPassword(REDIS_PASSWORD);
}
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(sentinelConfig);
return jedisConnectionFactory;
} catch (redis.clients.jedis.exceptions.JedisConnectionException re) {
logger.error("Unable to initialize Redis connection factory", re);
return null;
}
}
On the other:
public RedisTemplate getRedisTemplate() {
// We can assume that both methods are defined in the same class,
// although it is not necessary
final RedisConnectionFactory redisConnectionFactory = this.jedisConnectionFactory();
if (redisConnectionFactory == null) {
return null;
}
final RedisTemplate redisTemplate = new StringRedisTemplate(redisConnectionFactory);
return redisTemplate;
}
You can use this RedisTemplate
in the way you consider appropriate and, of course, cache and reuse it as necessary.
These methods can be defined in a service or helper class created for this task and, of course, not in your configuration classes.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论