解決Redis連接無法正常釋放的問題
錯誤信息:
IllegalStateException: Invalidated object not currently part of this pool
一、問題描述
前些天用多線程執(zhí)行操作測試驗證vanyar-redis連接池,應(yīng)用是剛重啟的狀態(tài),執(zhí)行操作是,開啟10個線程同時執(zhí)行10000次操作。
如下:
執(zhí)行操作完畢后發(fā)現(xiàn)控制臺輸出9個下面錯誤信息:
該錯誤大致意思是說:不能將redis連接放回池內(nèi),放回連接池的對象是無效的對象。在網(wǎng)上查了很多同類錯誤,都說是進(jìn)行了兩次returnResource釋放連接資源造成的,因為第一次return成功以后,第二次return就會報上面這個錯誤。但是顯然,我翻遍了代碼并沒有兩次調(diào)用returnResource。
查看redis服務(wù)端的連接數(shù)詳細(xì)信息如下,前9個連接,idle=453,空閑了453秒了,依然沒有釋放,而連接池設(shè)置的是空閑60秒就會被釋放,明顯發(fā)生異常了。
初步懷疑是多線程執(zhí)行redis操作,初始化redis連接池有問題。于是重啟應(yīng)用,先執(zhí)行單線程redis操作,再執(zhí)行多線程redis操作,沒有發(fā)生上面的問題。redis服務(wù)端連接均能正常釋放。由此得出結(jié)論,當(dāng)線程池在未初始化的時候,由于多線程同時執(zhí)行redis連接池初始化工作引起的問題。
看代碼(RedisJedisPool未優(yōu)化之前):當(dāng)10個線程同時請求redis連接資源時,10個線程都發(fā)現(xiàn)連接池為空(因為創(chuàng)建連接池相比創(chuàng)建線程比較耗時),這時10個線程都各自初始化成功一個連接池,并從中取得redis連接,并執(zhí)行了redis操作。執(zhí)行完畢,returnResource的時候,由于此時pool變量的引用是最后一個線程初始化的連接池,前面9個線程獲得的redis連接并不屬于最后一個連接池的資源,所以拋錯:IllegalStateException: Invalidated object not currently part of this pool
二、報錯原因分析
線程1 : 創(chuàng)建redis連接池1 : 獲得redis連接1
線程2 : 創(chuàng)建redis連接池2 : 獲得redis連接2
線程3 : 創(chuàng)建redis連接池3 : 獲得redis連接3
……
線程8 : 創(chuàng)建redis連接池8 : 獲得redis連接8
線程9 : 創(chuàng)建redis連接池9 : 獲得redis連接9
線程10 : 創(chuàng)建redis連接池10 : 獲得redis連接10
全局變量pool引用 指向 redis連接池10
當(dāng)線程1-9 把redis連接1-9 歸還給pool-redis連接池10
reds連接池10自然就報錯,說:
IllegalStateException: Invalidated object not currently part of this pool
三、解決辦法
由于創(chuàng)建線程池,連接池等工作都是相對比較耗時的,所以我們一般放在應(yīng)用啟動的時候就初始化,把連接池的初始化工作交給Spring容器管理,同時把初始化連接池和獲取連接兩個操作實現(xiàn)方法分離,對初始化連接池的方法加上同步鎖機(jī)制,并且二次判斷是否為空,就算多線程情況下,在二次判斷是否為空的時候,pool已經(jīng)不為空了,直接返回。現(xiàn)在多線程安全的問題就得以解決。
附上,解決前后對比圖:
補充知識:java spring框架中方法級redis的連接自動獲取和釋放實現(xiàn)
java中使用redis總是需要處理redis連接的獲取,釋放等操作,每次使用都會使代碼變的特別丑陋,模仿spring中aop的實現(xiàn),用動態(tài)代理寫一個 連接自動獲取和釋放的工具
主要思路
JedisManageSupport 抽象類 類似于 aop的切入點,所有繼承了該類(一般都是service層)的類,可以使用提供的獲取redis的方法獲取redis,并且不需要釋放
JedisBeanPostProcessor 繼承BeanPostProcessor ,會在bean初始化時執(zhí)行自己定義的邏輯:
如果A類繼承了 JedisManageSupport ,就會獲取redis連接并且放到JedisManageSupport 的成員變量里,A類的實例(其實是cglib動態(tài)代理生成的
A類的子類的實例)就可以使用該redis連接 進(jìn)行相關(guān)操作了
代理類的實例見源碼
源碼如下
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("調(diào)用之前注入jedis對象,method:" + method); /** * 下面代碼可以使用 method.invoke(src,arguments)。 不能使用 * methodProxy.invokeSuper(target,arguments); * 因為A類中用Autowired注入的屬性,生成代理的子類B后,因為子類B是新建的類。從父類繼承的屬性沒有被初始化, * 使用methodProxy.invokeSuper()執(zhí)行是,會報空指針異常. */ 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("調(diào)用之后歸還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); } /** * 如果某個鍵不同單位之間也不會重復(fù),可以使用這個方法生成redis的鍵 */ public final byte[] assemKey(String baseKey) { Assert.isTrue(StringUtils.isNotBlank(baseKey), "參數(shù)不能為空"); return baseKey.getBytes(); } /** * 根據(jù)tableName+prefix 構(gòu)造唯一key與assemKey(String baseKey, String tableName) * 規(guī)則一致 */ public final byte[] assemKeyByPrefix(String tableName, String baseKey) { Assert.isTrue(StringUtils.isNotBlank(baseKey), "參數(shù)不能為空"); Assert.isTrue(StringUtils.isNotBlank(tableName), "參數(shù)不能為空"); UnitInfo unit = WebService.getUnitInfo(); Assert.isTrue(unit != null, "單位信息獲取不到"); return (tableName + "-" + unit.getPrefix() + "-" + baseKey).getBytes(); } /** * * 不同前綴的表中可能有相同的鍵,同一個表中也可能是有重復(fù)的baseKey時,用這個生成redis的key 比如 用戶信息表的 * username字段,不同的用戶信息表允許重復(fù)的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連接無法正常釋放的問題就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
redis服務(wù)器允許遠(yuǎn)程主機(jī)訪問的方法
今天小編就為大家分享一篇redis服務(wù)器允許遠(yuǎn)程主機(jī)訪問的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-05-05Redis客戶端連接遠(yuǎn)程Redis服務(wù)器方式
這篇文章主要介紹了Redis客戶端連接遠(yuǎn)程Redis服務(wù)器方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-06-06redisson中RRateLimiter分布式限流器的使用
Redisson Ratelimiter是Redisson框架中的一種限流算法,用于限制對資源的訪問頻率,本文主要介紹了redisson中RRateLimiter分布式限流器的使用,感興趣的可以了解一下2024-06-06淺析Redis Sentinel 與 Redis Cluster
本文主要介紹Redis Sentinel 及 Redis Cluster的區(qū)別及用法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-06-06