解決Redis連線無法正常釋放的問題
錯誤資訊:
IllegalStateException: Invalidated object not currently part of this pool
一、問題描述
前些天用多執行緒執行操作測試驗證vanyar-redis連線池,應用是剛重啟的狀態,執行操作是,開啟10個執行緒同時執行10000次操作。
如下:
執行操作完畢後發現控制檯輸出9個下面錯誤資訊:
該錯誤大致意思是說:不能將redis連線放回池內,放回連線池的物件是無效的物件。在網上查了很多同類錯誤,都說是進行了兩次returnResource釋放連線資源造成的,因為第一次return成功以後,第二次return就會報上面這個錯誤。但是顯然,我翻遍了程式碼並沒有兩次呼叫returnResource。
檢視redis服務端的連線數詳細資訊如下,前9個連線,idle=453,空閒了453秒了,依然沒有釋放,而連線池設定的是空閒60秒就會被釋放,明顯發生異常了。
初步懷疑是多執行緒執行redis操作,初始化redis連線池有問題。於是重啟應用,先執行單執行緒redis操作,再執行多執行緒redis操作,沒有發生上面的問題。redis服務端連線均能正常釋放。由此得出結論,當執行緒池在未初始化的時候,由於多執行緒同時執行redis連線池初始化工作引起的問題。
看程式碼(RedisJedisPool未優化之前):當10個執行緒同時請求redis連線資源時,10個執行緒都發現連線池為空(因為建立連線池相比建立執行緒比較耗時),這時10個執行緒都各自初始化成功一個連線池,並從中取得redis連線,並執行了redis操作。執行完畢,returnResource的時候,由於此時pool變數的引用是最後一個執行緒初始化的連線池,前面9個執行緒獲得的redis連線並不屬於最後一個連線池的資源,所以拋錯:IllegalStateException: Invalidated object not currently part of this pool
二、報錯原因分析
執行緒1 : 建立redis連線池1 : 獲得redis連線1
執行緒2 : 建立redis連線池2 : 獲得redis連線2
執行緒3 : 建立redis連線池3 : 獲得redis連線3
……
執行緒8 : 建立redis連線池8 : 獲得redis連線8
執行緒9 : 建立redis連線池9 : 獲得redis連線9
執行緒10 : 建立redis連線池10 : 獲得redis連線10
全域性變數pool引用 指向 redis連線池10
當執行緒1-9 把redis連線1-9 歸還給pool-redis連線池10
reds連線池10自然就報錯,說:
IllegalStateException: Invalidated object not currently part of this pool
三、解決辦法
由於建立執行緒池,連線池等工作都是相對比較耗時的,所以我們一般放在應用啟動的時候就初始化,把連線池的初始化工作交給Spring容器管理,同時把初始化連線池和獲取連線兩個操作實現方法分離,對初始化連線池的方法加上同步鎖機制,並且二次判斷是否為空,就算多執行緒情況下,在二次判斷是否為空的時候,pool已經不為空了,直接返回。現在多執行緒安全的問題就得以解決。
附上,解決前後對比圖:
補充知識:java spring框架中方法級redis的連線自動獲取和釋放實現
java中使用redis總是需要處理redis連線的獲取,釋放等操作,每次使用都會使程式碼變的特別醜陋,模仿spring中aop的實現,用動態代理寫一個 連線自動獲取和釋放的工具
主要思路
JedisManageSupport 抽象類 類似於 aop的切入點,所有繼承了該類(一般都是service層)的類,可以使用提供的獲取redis的方法獲取redis,並且不需要釋放
JedisBeanPostProcessor 繼承BeanPostProcessor ,會在bean初始化時執行自己定義的邏輯:
如果A類繼承了 JedisManageSupport ,就會獲取redis連線並且放到JedisManageSupport 的成員變數裡,A類的例項(其實是cglib動態代理生成的
A類的子類的例項)就可以使用該redis連線 進行相關操作了
代理類的例項見原始碼
原始碼如下
public class JedisBeanPostProcessor implements BeanPostProcessor { @Autowired ShardedJedisPool shardedJedisPool; static final Logger logger = Logger.getLogger(JedisBeanPostProcessor.class); @Override public Object postProcessAfterInitialization(Object bean,String beanName) throws BeansException { if (bean instanceof JedisManageSupport) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(bean.getClass()); enhancer.setCallback(new JedisInterceptor(shardedJedisPool,bean)); Object targetBean = enhancer.create(); return targetBean; } else { return bean; } } @Override public Object postProcessBeforeInitialization(Object bean,String beanName) throws BeansException { return bean; } } class JedisInterceptor implements MethodInterceptor { static final Logger logger = Logger.getLogger(JedisInterceptor.class); ShardedJedisPool pool; Object src; public JedisInterceptor(ShardedJedisPool pool,Object src) { this.pool = pool; this.src = src; } @Override public Object intercept(Object target,Method method,Object[] arguments,MethodProxy methodProxy) throws Throwable { Object result = null; if (target instanceof JedisManageSupport) { if (this.isDeclaredMethod(target,method)) { ShardedJedis jedis = null; try { JedisManageSupport support = (JedisManageSupport) src; jedis = pool.getResource(); support.setShardedJedis(jedis); // logger.debug("呼叫之前注入jedis物件,method:" + method); /** * 下面程式碼可以使用 method.invoke(src,arguments)。 不能使用 * methodProxy.invokeSuper(target,arguments); * 因為A類中用Autowired注入的屬性,生成代理的子類B後,因為子類B是新建的類。從父類繼承的屬性沒有被初始化, * 使用methodProxy.invokeSuper()執行是,會報空指標異常. */ result = methodProxy.invoke(src,arguments); support.setShardedJedis(null); } catch (Exception e) { pool.returnBrokenResource(jedis); e.printStackTrace(); } finally { if (jedis != null) { pool.returnResource(jedis); } // logger.debug("呼叫之後歸還jedis物件,method:" + method); } } else { result = methodProxy.invoke(src,arguments); } } else { throw new Exception("使用該代理必須繼承JedisManageSupport"); } return result; } /** * 是否是target類本身定義的非私有的方法,還是繼承的父類 * @return true是target自己類的並且不是私有的的, */ private boolean isDeclaredMethod(Object target,Method arg1) { Method temp = null; try { temp = target.getClass().getDeclaredMethod(arg1.getName(),arg1.getParameterTypes()); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } /** * 不為null,並且是非私有的,返回true */ if (temp != null) { return true; } else { return false; } } } public abstract class JedisManageSupport { ThreadLocal<ShardedJedis> jedisHolder = new ThreadLocal<ShardedJedis>(); public final ShardedJedis getShardedJedis() { return jedisHolder.get(); } public final void setShardedJedis(ShardedJedis jedis) { jedisHolder.set(jedis); } /** * 如果某個鍵不同單位之間也不會重複,可以使用這個方法生成redis的鍵 */ public final byte[] assemKey(String baseKey) { Assert.isTrue(StringUtils.isNotBlank(baseKey),"引數不能為空"); return baseKey.getBytes(); } /** * 根據tableName+prefix 構造唯一key與assemKey(String baseKey,String tableName) * 規則一致 */ public final byte[] assemKeyByPrefix(String tableName,String baseKey) { Assert.isTrue(StringUtils.isNotBlank(baseKey),"引數不能為空"); Assert.isTrue(StringUtils.isNotBlank(tableName),"引數不能為空"); UnitInfo unit = WebService.getUnitInfo(); Assert.isTrue(unit != null,"單位資訊獲取不到"); return (tableName + "-" + unit.getPrefix() + "-" + baseKey).getBytes(); } /** * * 不同字首的表中可能有相同的鍵,同一個表中也可能是有重複的baseKey時,用這個生成redis的key 比如 使用者資訊表的 * username欄位,不同的使用者資訊表允許重複的username,mooc_t_userinfo * 也允許有相同的賬號,所以生成redis的key時,需要用到單位的mooc_school 放入redis中 */ public final byte[] assemKeyByFid(String tableName,String baseKey) { UnitInfo unit = WebService.getUnitInfo(); Assert.isTrue(unit != null,"單位資訊獲取不到"); return (tableName + "-" + unit.getMoocSchool() + "-" + baseKey).getBytes(); } }
以上這篇解決Redis連線無法正常釋放的問題就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。