redis scan命令導(dǎo)致redis連接耗盡,線程上鎖的解決
使用redis scan方法無(wú)法獲取connection,導(dǎo)致線程鎖死。
0、關(guān)鍵字
redis
springboot
redistemplate
scan
try-with-resource
1、異?,F(xiàn)象
應(yīng)用部署后,功能正常使用,但約數(shù)小時(shí)左右,部分功能接口異常,接口請(qǐng)求無(wú)響應(yīng)。
2、異常排查
查看堆棧信息,jstask pid。首先找到j(luò)ava進(jìn)程pid;輸出堆棧信息至log文件,jstask 30 > stask.log,看到與redis相關(guān)的日志,線程狀態(tài)為waiting。
"pool-13-thread-6" prio=10 tid=0x00007f754800e800 nid=0x71b5 waiting on condition [0x00007f758f0ee000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x0000000779b75f40> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043) at org.apache.commons.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:583) at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:442) at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363) at redis.clients.util.Pool.getResource(Pool.java:49) at redis.clients.jedis.JedisPool.getResource(JedisPool.java:99) at org.reborndb.reborn.RoundRobinJedisPool.getResource(RoundRobinJedisPool.java:300) at com.le.smartconnect.adapter.spring.RebornConnectionFactory.getConnection(RebornConnectionFactory.java:43) at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:128) at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:91) at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:78) at xxx.run(xxx.java:80) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745) Locked ownable synchronizers: - <0x000000074f529b08> (a java.util.concurrent.ThreadPoolExecutor$Worker)
也就是說,redis連接獲取不到,線程一直在等待可用的redis連接。大概率是應(yīng)用中有功能模塊獲取到連接,并沒有釋放。找到一個(gè)功能使用了scan,具體如下:
public void releaseCallbackMessage() throws Exception { Cursor<Map.Entry<Object, Object>> cursor = RedisCacheUtils.scan(key) if (cursor == null) { logger.info("通過scan(H key, ScanOptions options)方法獲取匹配鍵值對(duì)記錄為空"); return; } while (cursor.hasNext()) { // 遍歷緩存 Map.Entry<Object, Object> entry = cursor.next(); String key = String.valueOf(entry.getKey()); } } }
查看scan源碼,發(fā)現(xiàn)其使用過程中,并未主動(dòng)釋放connection,而get/set操作均會(huì)主動(dòng)釋放connection
public Cursor<Entry<HK, HV>> scan(K key, ScanOptions options) { byte[] rawKey = rawKey(key); return template.executeWithStickyConnection( (RedisCallback<Cursor<Entry<HK, HV>>>) connection -> new ConvertingCursor<>(connection.hScan(rawKey, options), new Converter<Entry<byte[], byte[]>, Entry<HK, HV>>() { @Override public Entry<HK, HV> convert(final Entry<byte[], byte[]> source) { return new Entry<HK, HV>() { @Override public HK getKey() { return deserializeHashKey(source.getKey()); } @Override public HV getValue() { return deserializeHashValue(source.getValue()); } @Override public HV setValue(HV value) { throw new UnsupportedOperationException("Values cannot be set when scanning through entries."); } }; } })); }
get操作源碼finally中有releaseConnection操作。
@Nullable public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) { Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it"); Assert.notNull(action, "Callback object must not be null"); RedisConnectionFactory factory = getRequiredConnectionFactory(); RedisConnection conn = null; try { if (enableTransactionSupport) { // only bind resources in case of potential transaction synchronization conn = RedisConnectionUtils.bindConnection(factory, enableTransactionSupport); } else { conn = RedisConnectionUtils.getConnection(factory); } boolean existingConnection = TransactionSynchronizationManager.hasResource(factory); RedisConnection connToUse = preProcessConnection(conn, existingConnection); boolean pipelineStatus = connToUse.isPipelined(); if (pipeline && !pipelineStatus) { connToUse.openPipeline(); } RedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse)); T result = action.doInRedis(connToExpose); // close pipeline if (pipeline && !pipelineStatus) { connToUse.closePipeline(); } // TODO: any other connection processing? return postProcessResult(result, connToUse, existingConnection); } finally { RedisConnectionUtils.releaseConnection(conn, factory); } }
3、解決方式
scan操作后,主動(dòng)關(guān)閉游標(biāo),使用try(resource) catch(exception)方式編碼。
1、redis scan操作記住需要主動(dòng)關(guān)閉cursor,即cursor.close;
2、加強(qiáng)規(guī)范編碼;
try (Cursor<Map.Entry<Object, Object>> cursor = RedisCacheUtils.scan(key)) { if (cursor == null) { logger.info("通過scan(H key, ScanOptions options)方法獲取匹配鍵值對(duì)記錄為空"); return; } while (cursor.hasNext()) { // 遍歷緩存 Map.Entry<Object, Object> entry = cursor.next(); String key = String.valueOf(entry.getKey()); } } catch (Exception ex) { logger.info(ex.toString()); }
關(guān)于 try-with-resources用法需要提一點(diǎn)的就是,resources對(duì)象必須是實(shí)現(xiàn)了 java.lang.AutoCloseable接口,才會(huì)自動(dòng)關(guān)閉對(duì)象。
補(bǔ)充知識(shí):redis連接未釋放,導(dǎo)致redis連接池滿,從而應(yīng)用服務(wù)不可用的問題定位和解決
版本提交測(cè)試驗(yàn)收后,跑了幾天,今天測(cè)試突然跑來說平臺(tái)不可用。
1. 我先是試圖登錄平臺(tái),發(fā)現(xiàn)首頁(yè)可以進(jìn)入,但是登錄不成功。很顯然是后臺(tái)的問題。
2. 再看MQ中,發(fā)現(xiàn)消息堆積在隊(duì)列中,未被消費(fèi)掉,同時(shí)一點(diǎn)一點(diǎn)變化,說明很有可能是哪里有內(nèi)存或連接的泄露或未釋放。
3. 接著登錄阿里云賬號(hào),查看redis監(jiān)控,發(fā)現(xiàn)連接數(shù)已經(jīng)達(dá)到9000多。
4. 查看日志發(fā)現(xiàn)大量的redis連接拒絕錯(cuò)誤
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool at redis.clients.util.Pool.getResource(Pool.java:42) at redis.clients.jedis.JedisPool.getResource(JedisPool.java:84) at com.***(**.java:58) at com.***(**.java:86) at com.***(**.java:27) at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:251) at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:66) at org.apache.log4j.Category.callAppenders(Category.java:206) at org.apache.log4j.Category.forcedLog(Category.java:391) at org.apache.log4j.Category.log(Category.java:856) at org.slf4j.impl.Log4jLoggerAdapter.error(Log4jLoggerAdapter.java:571) at com.***(**.java:61) at com.***(**.java:86) at com.***(**.java:27) at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:251) at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:66) at org.apache.log4j.Category.callAppenders(Category.java:206) at org.apache.log4j.Category.forcedLog(Category.java:391) at org.apache.log4j.Category.log(Category.java:856) at org.slf4j.impl.Log4jLoggerAdapter.error(Log4jLoggerAdapter.java:571) at com.***(**.java:61) at com.***(**.java:86) at com.***(**.java:27) at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:251)
5. 當(dāng)然后臺(tái)的tomcat中也報(bào)了其他錯(cuò)誤,比如:
Exception in thread "Thread-18" java.lang.StackOverflowError at java.util.Hashtable.get(Hashtable.java:367) at java.util.Properties.getProperty(Properties.java:969) at java.lang.System.getProperty(System.java:720) at sun.security.action.GetPropertyAction.run(GetPropertyAction.java:86) at sun.security.action.GetPropertyAction.run(GetPropertyAction.java:52) at java.security.AccessController.doPrivileged(Native Method) at java.io.PrintWriter.<init>(PrintWriter.java:116) at java.io.PrintWriter.<init>(PrintWriter.java:100) at org.apache.log4j.DefaultThrowableRenderer.render(DefaultThrowableRenderer.java:58) at org.apache.log4j.spi.ThrowableInformation.getThrowableStrRep(ThrowableInformation.java:87) at com.aliyun.openservices.log.log4j.LoghubAppender.append(LoghubAppender.java:116) at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:251) at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:66) at org.apache.log4j.Category.callAppenders(Category.java:206) at org.apache.log4j.Category.forcedLog(Category.java:391) at org.apache.log4j.Category.log(Category.java:856) at org.slf4j.impl.Log4jLoggerAdapter.error(Log4jLoggerAdapter.java:571) at com.***(**.java:61) at com.***(**.java:86) at com.***(**.java:27) at org.apache.log4j.AppenderSkeleton.doAppend(AppenderSkeleton.java:251) at org.apache.log4j.helpers.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:66) at org.apache.log4j.Category.callAppenders(Category.java:206) at org.apache.log4j.Category.forcedLog(Category.java:391) at org.apache.log4j.Category.log(Category.java:856)
6. 但是,還是繼續(xù)查看日志,發(fā)現(xiàn)一直有個(gè) [TaskId]PUPHVUNVJSSMKOTQPKHRSPOMUKKOKLPG [Message]null [Result]0 debug日志,因?yàn)橹挥幸恢辈煌5陌l(fā),且連接不關(guān)閉才可能出現(xiàn)這么多連接的情況。一邊對(duì)應(yīng)代碼,發(fā)現(xiàn)是頁(yè)面上調(diào)用后臺(tái)代碼,發(fā)送給設(shè)備長(zhǎng)連接的代碼。
try{ for(...) { jedis = RedisManagerPool.getJedis(); if(!"NULL".equals(value)) { break; } RedisUtils.return(jedis); } } catch() { logger.error(); RedisUtils.returnBroken(jedis); } return value;
且try中沒有finally塊,很顯然如果條件滿足的話就直接break,并return value。但是RedisUtils.return(jedis)這條語(yǔ)句就未執(zhí)行。然后進(jìn)一步懷疑頁(yè)面是否是定時(shí)去獲取,通過F12,發(fā)現(xiàn)每10s鐘請(qǐng)求一次,頁(yè)面上要獲取設(shè)備的上下行速率。所以會(huì)累積這么多的請(qǐng)求。最后,修改也比較簡(jiǎn)單,添加finally塊,保證RedisUtils.return(jedis)必定會(huì)執(zhí)行。
8. 接下來在另一個(gè)開發(fā)環(huán)境繼續(xù)復(fù)現(xiàn),我們將redisManagerPool中設(shè)置maxTotal=300,maxIdle=30。而原先是3000、300,這樣有利于快速?gòu)?fù)現(xiàn)。
果然,一上午時(shí)間就達(dá)到了300的限制。出現(xiàn)了一樣的問題。
9. 綜上,問題定位清楚,且修復(fù)該問題。
a) 對(duì)于oss redis之類的第三方網(wǎng)絡(luò)連接,必須要有finally塊執(zhí)行。否則后續(xù)很容易由于不規(guī)范的編碼,出現(xiàn)這種連接未正常釋放的問題。
b) 定位問題,還是需要有日志。如果單從代碼去查,方向會(huì)比較多且很容易浪費(fèi)時(shí)間。
c) 修改池大小,縮短復(fù)現(xiàn)時(shí)間,快速定位修改。
以上這篇redis scan命令導(dǎo)致redis連接耗盡,線程上鎖的解決就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Cloud Zuul路由網(wǎng)關(guān)服務(wù)過濾實(shí)現(xiàn)代碼
這篇文章主要介紹了Spring Cloud Zuul路由網(wǎng)關(guān)服務(wù)過濾實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04Springboot接收文件與發(fā)送文件實(shí)例教程
最近工作中遇到個(gè)需求,springboot簡(jiǎn)單的上傳文檔或者圖片,并且進(jìn)行操作,操作完后進(jìn)行保存指定路徑,下面這篇文章主要給大家介紹了關(guān)于Springboot接收文件與發(fā)送文件的相關(guān)資料,需要的朋友可以參考下2023-05-05Java將String字符串帶括號(hào)轉(zhuǎn)成List的簡(jiǎn)單方法
Java中我們有時(shí)需要對(duì)現(xiàn)有的字符串進(jìn)行切割并轉(zhuǎn)化成一個(gè)List集合,這篇文章主要給大家介紹了關(guān)于Java將String字符串帶括號(hào)轉(zhuǎn)成List的簡(jiǎn)單方法,需要的朋友可以參考下2023-03-03如何解決Spring in action @valid驗(yàn)證不生效的問題
這篇文章主要介紹了如何解決Spring in action @valid驗(yàn)證不生效的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06如何查看Linux上正在運(yùn)行的所有Java程序列表
在linux操作時(shí),經(jīng)常要查看運(yùn)行的項(xiàng)目的進(jìn)程和端口,下面這篇文章主要給大家介紹了關(guān)于如何查看Linux上正在運(yùn)行的所有Java程序列表的相關(guān)資料,需要的朋友可以參考下2023-10-10Spring?Boot指標(biāo)監(jiān)控及日志管理示例詳解
Spring Boot Actuator可以幫助程序員監(jiān)控和管理SpringBoot應(yīng)用,比如健康檢查、內(nèi)存使用情況統(tǒng)計(jì)、線程使用情況統(tǒng)計(jì)等,這篇文章主要介紹了Spring?Boot指標(biāo)監(jiān)控及日志管理,需要的朋友可以參考下2023-11-11