問題現象(jedis-2.1.0.jar)
基於JedisPool管理Jedis對象,通過get方法獲取值,出現key對應的value值錯誤,例如:
K V
a a
b b
Jedis.get(“a”)==’b’;
通過獲取key為a的值,但獲取了值b來。
同一套代碼的項目,分別部署在兩個不同的應用集群,其中一個集群出現這種問題,而另一個集群卻沒有出現。
問題分析
通過表象可以看出,應該是鏈接池的Jedis對象鏈接出現錯亂而導致的。而兩個集群中的其中一個集群出現,這兩個集群的唯一區別就是網絡環境不一樣,所以連接Redis服務器的網絡是有差別的。
問題思考
根據以上信息,可以大致判斷出應該跟網速和JedisPool鏈接池的超時時間(500毫秒)設置有關。那接下來的問題是,如果網絡差的集群,出現redis連接超時,那么Jedis為什么會錯誤呢?是否在連接池的配置不當知道呢?
問題發現
帶着以上的疑問,繼續Google和百度相關資料,結果發現returnBrokenResource這個方法。通過資料查找和對JedisPool的源碼分析,此方法是銷毀異常Jedis連接的。如果Jedis鏈接發現異常(如連接超時),不對異常連接銷毀的話,會有數據緩存問題。
異常流程:
重現問題測試代碼(設置1ms的readTimeOut時間,以便問題重現):
1 package test; 2
3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.util.Properties; 6 import java.util.Random; 7 import org.apache.commons.lang.StringUtils; 8 import redis.clients.jedis.Jedis; 9 import redis.clients.jedis.JedisPool; 10 import redis.clients.jedis.JedisPoolConfig; 11
12 public class RedisTest implements Runnable{ 13
14 public static JedisPool pool = null; 15
16 static { 17 try { 18 JedisPoolConfig config = new JedisPoolConfig(); 19 config.setMaxActive(100); 20 config.setMaxIdle(10); 21 config.setMaxWait(1000); 22 config.setTestOnBorrow(false); 23 config.setTestOnReturn(false); 24 config.setTestWhileIdle(true); 25 config.setTimeBetweenEvictionRunsMillis(30000); 26 config.setNumTestsPerEvictionRun(10); 27 config.setMinEvictableIdleTimeMillis(60000); 28 pool = new JedisPool(config, "192.168.22.213", 6379,1); 29 } catch (Exception e) { 30 System.out.println("【jedispool init error】"); 31 } 32 } 33
34 public void run() { 35
36 Jedis jedis = null; 37 String result = ""; 38 int i = new Random().nextInt(1000); 39
40 try{ 41 jedis=pool.getResource(); 42 result = jedis.get("T"+i); 43
44 if(StringUtils.isNotEmpty(result) && !result.equals("T"+i)){ 45 System.out.println(result+"!=T"+i); 46 } 47
48 }catch(Exception e){ 49 System.out.println(jedis+e.toString()); 50
51 }finally{ 52 if(jedis!=null){ 53 pool.returnResource(jedis); 54 } 55 } 56
57 } 58
59 /**
60 * 模擬2000線程並發 61 */
62 public static void main(String[] args) throws Exception { 63
64 for(int i=0;i<2000;i++){ 65 new Thread(new RedisTest()).start(); 66 } 67 } 68 }
執行結果:
……
T50!=T47
redis.clients.jedis.Jedis@1295fe8redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
T56!=T94
redis.clients.jedis.Jedis@151b0a5redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
T717!=T380
redis.clients.jedis.Jedis@131303fredis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
redis.clients.jedis.Jedis@602b6bredis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
T204!=T787
T474!=T763
T163!=T542
T552!=T60
T604!=T820
T733!=T624
redis.clients.jedis.Jedis@131303fredis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
redis.clients.jedis.Jedis@d56b37redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
redis.clients.jedis.Jedis@151b0a5redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
redis.clients.jedis.Jedis@1295fe8redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: Read timed out
T784!=T948
T440!=T672
T97!=T867
……
以上結果出現許多鍵值不對應的情況。
解決方案
當Jedis讀超時時,把此實例銷毀,以免造成后續傷害。
銷毀異常Jedis有三種方法:
方法1:加入紅色代碼,當讀取Redis數據時任何異常都拋棄此Jedis實例
try{ jedis=pool.getResource(); result = jedis.get("T"+i); if(StringUtils.isNotEmpty(result) && !result.equals("T"+i)){ System.out.println(result+"!=T"+i); } }catch(Exception e){ System.out.println(jedis+e.toString()); if(jedis!=null){ pool.returnBrokenResource(jedis); } }finally{ if(jedis!=null){ pool.returnResource(jedis); } }
方法2:配置JedisPool的TestOnBorrow為true
config.setTestOnBorrow(true);
方法3:配置JedisPool的TestOnReturn為true
config.setTestOnReturn(true);
總結
其實以上三種方法原理都是一樣,就是檢查Jedis的有效性,銷毀異常Jedis鏈接實例。只是檢查的時間不一樣。
而導致量應用集群中其中之一出現,可以定位為有問題集群到Redis集群服務器的網速比正常集群的差(500ms超時限制)
方法1是在發生時檢驗銷毀;
方法2是在從連接池獲取Jedis實例時檢查;
截圖源碼來自package org.apache.commons.pool.impl.GenericObjectPool
方法3是在歸還Jedis實例給連接池時檢查;
截圖源碼來自package org.apache.commons.pool.impl.GenericObjectPool
以上三種檢查連接有效性方法都是一致:
boolean isNormal = false;
try{
isNormal = (jedis.isConnected()) && (jedis.ping().equals("PONG"));
}catch(Exception e){
isNormal = false;
}
注1,三種方法可以同時使用,但需要在檢查性能消耗和功能穩定性之間衡量。
文章更新時間:2016-07-01
更新內容:當我把jedis更新到2.8.1的時候使用returnBrokenResource和returnResource顯示方法過期,原因是Jedis類重寫了這個close方法,原本是沒有的,它在close已經幫你判斷好,源碼如下所示:
public void close() { if (this.dataSource != null) { if (this.client.isBroken()) this.dataSource.returnBrokenResource(this); else this.dataSource.returnResource(this); } else this.client.close(); }