环境概述
1. SpringBoot 1.5.9 注解方式返回单例Jedis对象作为client
2.JedisPool连接配置如下:
max-total: 100 # 连接池最大连接数(使用负值表示没有限制)
max-wait: 10 # 连接池最大阻塞等待时间(使用负值表示没有限制)
min-idle: 10 # 连接池中的最小空闲连接
max-idle: 10 # 连接池中的最大空闲连接
问题描述
在执行reids操作过程中,一旦抛出异常,例如:RedisClient - java.net.SocketTimeoutException: Read timed out,接下来所有的redis操作都是失败的且无法恢复。
问题代码
public String get(String key) {
try {
return jedisClient.get(key);
} catch (Exception e) {
logger.error("get:{} have exception:{}", key, e);
}
return null;
}
原因分析
首先看获得和引用Jedis实例代码:
@Bean(name= "jedisClient")
@Autowired
public Jedis singleJedis(@Value("${spring.redis.host}") String host,
@Value("${spring.redis.port}") int port) {
return new JedisPool(jedisPoolConfig, host, port).getResource();
}
@Autowired
private RedisClient redisClient;
很明显可以看到,整个context都是一个Jedis实例持有这些连接。那么这个实例持有的当前连接异常断开时,应该怎么办呢?应该close()掉。因为close的时候会判断连接的可用性,来决定是否采用新的连接,代码如下:
public void close() {
if (this.dataSource != null) {
if (this.client.isBroken()) {//关键代码,判断是否broken
this.dataSource.returnBrokenResource(this); #执行归还broken调用
} else {
this.dataSource.returnResource(this);#执行归还调用
}
} else {
this.client.close();
}
}
/** 下面代码展示,broken的连接归还流程 */
protected void returnBrokenResourceObject(T resource) {
try {
this.internalPool.invalidateObject(resource);//调用无效对象方法
} catch (Exception var3) {
throw new JedisException("Could not return the resource to the pool", var3);
}
}
public void invalidateObject(T obj) throws Exception {
PooledObject<T> p = (PooledObject)this.allObjects.get(new IdentityWrapper(obj));
if (p == null) {
if (!this.isAbandonedConfig()) {
throw new IllegalStateException("Invalidated object not currently part of this pool");
}
} else {
synchronized(p) {
if (p.getState() != PooledObjectState.INVALID) { //这句代码关键,没有设置无效状态事进行destory操作
this.destroy(p);
}
}
this.ensureIdle(1, false);
}
}
//destory操作中对对象进行了删除和销毁操作
private void destroy(PooledObject<T> toDestroy) throws Exception {
toDestroy.invalidate();
this.idleObjects.remove(toDestroy);
this.allObjects.remove(new IdentityWrapper(toDestroy.getObject()));
try {
this.factory.destroyObject(toDestroy);
} finally {
this.destroyedCount.incrementAndGet();
this.createCount.decrementAndGet();
}
}
/** 下面的代码展示正常连接归还流程 */
public void returnResource(T resource) {
if (resource != null) {
this.returnResourceObject(resource);
}
}
public void returnResourceObject(T resource) {
if (resource != null) {
try {
this.internalPool.returnObject(resource);//关键代码,可以发现正常连接归还的流程最终并没有删除销毁,也就是说其实可以不归还,继续使用,更加高效
} catch (Exception var3) {
throw new JedisException("Could not return the resource to the pool", var3);
}
}
}
分析结论
在执行Redis操作异常时,catch到并执行close操作即可,代码如下:
public String get(String key) {
try {
return jedisClient.get(key);
} catch (Exception e) {
logger.error("get:{} have exception:{}", key, e);
jedisClient.close(); //添加此行代码
}
return null;
}
经过实际验证,可以解决异常无法自动恢复的问题,需要指出的是jedis始终是那一个实例,只是持有的连接不同了
更新:随着排查深入,发现在springboot 1.5.9中,是通过添加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
依赖来引用jedis,进一步查看jedis版本是2.9.0,而2.9.0版本已经是是较高的稳定版且是使用占比最大的版本:
实际上,在使用如此广泛的引用中,应该由jedis库再发现异常时,直接进行close(),而不是由业务方来完成此事。