环境概述

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版本已经是是较高的稳定版且是使用占比最大的版本:

bgsave redis异常 redis异常处理情况_redis

实际上,在使用如此广泛的引用中,应该由jedis库再发现异常时,直接进行close(),而不是由业务方来完成此事。