Jedis的類型轉換異常深究

1 類型轉換異常場景

咱們在使用Jedis的時候,常常會出現類型轉換異常,有以下狀況:redis

  • 多線程環境數組

    Jedis是線程不安全的,若是存在多線程使用同一個Jedis,就會出現類型轉換異常網上也流傳着不少錯誤的解釋,下面咱們以一個案例來複現下這個問題,這個很好理解。安全

  • 單線程環境服務器

    即便在單線程的狀況下,也是會出現類型轉換異常的,下面就針對此作一個案例分析微信

2 Jedis類型轉換異常案例

2.1 案例介紹

案例是從這裏來的Jedis returnResource使用注意事項網絡

代碼以下:多線程

public static void main(String[] args) throws Exception{
	Jedis jedis = new Jedis("192.168.126.131", 6379);
	System.out.println("get name=" + jedis.get("name"));
	System.out.println("Make SocketTimeoutException");
	System.in.read(); //等待制造SocketTimeoutException
	try {
	    System.out.println(jedis.get("name"));
	} catch (Exception e) {
	    e.printStackTrace();
	}
	System.out.println("Recover from SocketTimeoutException");
	Thread.sleep(50000); // 繼續休眠一段時間 等待網絡徹底恢復
	boolean isMember = jedis.sismember("urls", "baidu");
	System.out.println("isMember " + isMember);
	jedis.close();
}

以及包含2個阻斷和解除網絡通訊的命令框架

  • 阻斷網絡通訊異步

    sudo iptables -A INPUT -p tcp --dport 6379 -j DROP
  • 解除網絡阻塞tcp

    sudo iptables -F

案例運行過程描述:

  • 1 建立Jedis,發送get命令,啓動與redis的鏈接,鏈接成功後獲取到響應數據
  • 2 程序阻塞在System.in.read(),等待輸入,此時咱們須要將網絡鏈接阻塞,執行上述阻斷網絡命令
  • 3 輸入任意數據,讓程序再也不阻塞,繼續走下去,執行get命令,此時因爲網絡不通,致使出現SocketTimeoutException異常
  • 4 打印出異常,繼續往下走,sleep 50s,此時咱們須要解除網絡阻塞,執行上述對應命令
  • 5 50s過完,就會執行jedis的sismember方法,此時就會出現類型轉換異常

2.2 Jedis原理介紹

這裏再也不詳細介紹。

Jedis內部有一個Socket與redis服務器創建鏈接。在建立Jedis對象的時候,並無去創建鏈接,而是在執行命令的時候纔會先檢查是否已鏈接,未鏈接的話,才創建鏈接。

Socket一旦鏈接創建,就會獲取到Socket的OutputStream,並用RedisOutputStream進行包裝,獲取到Socket的InputStream,並用RedisInputStream進行包裝。RedisOutputStream內部含有一個byte buf[]數組。

也就是說在jedis在向OutputStream寫入命令的時候,會先寫入到上述buf數組中,而後在讀取的時候,纔會flush上述數據,將數據寫入到Socket的OutputStream中,並調用flush,以Jedis的get方法爲例

public String get(final String key) {
	checkIsInMulti();
	client.sendCommand(Protocol.Command.GET, key);
	return client.getBulkReply();
}

client.sendCommand方法會將數據寫入到RedisOutputStream內部的buf中 client.getBulkReply方法會首先執行一次flush,即將buf中數據寫入到Socket的OutputStream中,並調用Socket的OutputStream的flush。

2.3 類型轉換異常的緣由

網上不少人說形成上述場景的類型轉換異常是由於:

出現SocketTimeoutException異常後,RedisOutputStream的buf中殘留上次命令,沒作清理處理,致使再執行其餘命令時連同以前的命令一塊兒發送過去了。

通過查看RedisOutputStream的源碼,buf中確實不會去主動清除原有數據,而是每次都是直接覆蓋,有count指針來標記,可是這也不會形成上述所說的影響,RedisOutputStream是OK的。

首先咱們要明白什麼是SocketTimeoutException異常: 上述Jedis的Socket在發送完成數據後,就會去執行讀取數據,即讀取Socket的InputStream中的數據,而且又必定的阻塞時間,若是redis服務器遲遲不返回數據,一旦超過SO_TIMEOUT(即Socket的讀取超時時間),客戶端就會拋出一個SocketTimeoutException異常。

形成這種異常的緣由有不少:

  • 網絡閃斷(會TCP重傳):上述案例情景就是網絡斷開,數據包發送失敗,會TCP重傳
  • 網絡沒有斷,可是傳輸比較慢,或者redis服務器處理很慢

上述緣由都會形成客戶端讀取超時。一旦超時,咱們的Jedis程序拋出異常,繼續往下走,若是此時再次執行其餘命令的話,仍然會讀取服務器端響應,此時讀到的響應就是上次請求的響應了,因此會致使類型轉換異常。若是與上次請求的類型一致,那就更可怕了,錯誤就會被深深的掩蓋過去了。

3 Jedis類型轉換異常的解決辦法

上述問題就是:咱們沒有正確對待這個SocketTimeoutException異常,即一旦出現SocketTimeoutException異常,咱們是必需要廢棄掉這個Jedis的。因此對於單線程環境下的Jedis來講,一旦出現這種異常,咱們須要從新new一個新的Jedis來使用。

Jedis在內部執行出現異常,如SocketTimeoutException異常的時候,會標記一個boolean broken=true,即意味着該鏈接已經廢棄了。

重要的大坑在這裏,咱們一般使用JedisPool來應對多線程環境下Jedis的使用,通常使用方式以下:

Jedis jedis = null;//從pool中獲取資源  
try{
	jedis = pool.getResource();
    jedis.set("k1", "v1");  
}catch(Exception e){  
    e.printStackTrace();
}finally{
	if(jedis != null){
		pool.returnResource(jedis);//向鏈接池「歸還」資源,千萬不要忘記。
	}
}

而對於JedisPool,咱們會使用returnResource方法來向pool中釋放回Jedis,而這個returnResource卻忽視了上述boolean broken屬性,直接將一個標記廢棄的鏈接放回到了pool中,下次別人取的時候,必然出問題。

因此針對JedisPool這種狀況,解決辦法以下:

  • 1 在上述catch中捕獲SocketTimeoutException異常,調用pool的returnBrokenResource方法來釋放Jedis(該方法會將Jedis實例標記爲下線,沒法被他人獲取到了),可是不推薦這種,還要考慮其餘異常等等

  • 2 另外一個就是直接調用Jedis的close方法,最新版2.9.0(其餘版本沒驗證)中close方法對上述boolean broken標記進行了處理,而且將returnResource標記成廢棄了,處理以下

    public void close() {
    	if (dataSource != null) {
    	  if (client.isBroken()) {
    	    this.dataSource.returnBrokenResource(this);
    	  } else {
    	    this.dataSource.returnResource(this);
    	  }
    	} else {
    	  client.close();
    	}
    }

上述this.dataSource能夠理解爲JedisPool。 即一旦是broken,則調用pool的returnBrokenResource方法,不然調用pool的returnResource方法。

因此最終寫法應該以下:

Jedis jedis = null;//從pool中獲取資源  
try{
	jedis = pool.getResource();
    jedis.set("k1", "v1");  
}finally{
	if(jedis != null){
		jedis.close();
	}
}

4 問題深思

能夠想到2方面的問題:

問題1:jedis爲何要暴漏這麼個危險的API給用戶使用(即要求用戶自覺的close,不自覺後果自負)

若是是咱們在開發框架給被人使用,那就要儘可能避免這種API的設計,把close自動隱藏在框架內部,避免了使用人員的誤使用,同時減小了代碼的複雜度,即便是上述最終的寫法也是很醜陋的,要完成一個set功能,要關注太多地方了,這部分徹底能夠框架底層包裝起來,只給用戶一個set方法便可。

問題2:請求和響應的不匹配問題

這種不匹配的問題在同步和異步的時候分別怎麼處理?

同步通訊:在設計的時候,必須發送一次請求就要讀取一次響應,經過這種方式來匹配。然而在某些狀況下,讀取響應有必定的超時時間,一旦超時,就拋出SocketTimeoutException異常,從而結束本次讀取,而響應可能後來又到達了,這種狀況就會形成不匹配的現象。要避免這種狀況,就必需要廢棄掉這個Socket了,因此若是客戶端設計成同步通訊的時候,一旦遇到這種異常,則就須要廢棄了,從新創建鏈接了。

異步通訊:在設計的時候通常會爲每一個請求分配一個請求id,服務器端在處理請求後,會把這個請求id返回給客戶端,客戶端根據返回的請求id來匹配是那一次的請求對應的響應,就不會出現上述那種匹配錯亂的問題。異步通訊在讀取數據的時候也一般是有數據可讀纔會去執行讀操做,能夠減小同步通訊中因網絡擁堵或其餘緣由形成的SocketTimeoutException問題。異步通訊好處的代價就是比同步通訊複雜。

因此若是咱們在設計的時候,就須要去考慮這樣的問題,避免造出一個大坑來。

歡迎關注微信公衆號:乒乓狂魔

微信公衆號

相關文章
相關標籤/搜索