Redis client之Jedis線上程執行丟擲異常無法恢復的情形和解決方案

FeelTouch發表於2018-12-12

環境概述

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(),而不是由業務方來完成此事。

更多資訊可參考:https://my.oschina.net/jrrx/blog/1634837

相關文章