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