redis scan命令导致redis连接耗尽,线程上锁的解决
作者:loongshawn 发布时间:2021-11-19 02:57:52
使用redis scan方法无法获取connection,导致线程锁死。
0、关键字
redis
springboot
redistemplate
scan
try-with-resource
1、异常现象
应用部署后,功能正常使用,但约数小时左右,部分功能接口异常,接口请求无响应。
2、异常排查
查看堆栈信息,jstask pid。首先找到java进程pid;输出堆栈信息至log文件,jstask 30 > stask.log,看到与redis相关的日志,线程状态为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连接。大概率是应用中有功能模块获取到连接,并没有释放。找到一个功能使用了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)方法获取匹配键值对记录为空");
return;
}
while (cursor.hasNext()) {
// 遍历缓存
Map.Entry<Object, Object> entry = cursor.next();
String key = String.valueOf(entry.getKey());
}
}
}
查看scan源码,发现其使用过程中,并未主动释放connection,而get/set操作均会主动释放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操作后,主动关闭游标,使用try(resource) catch(exception)方式编码。
1、redis scan操作记住需要主动关闭cursor,即cursor.close;
2、加强规范编码;
try (Cursor<Map.Entry<Object, Object>> cursor = RedisCacheUtils.scan(key)) {
if (cursor == null) {
logger.info("通过scan(H key, ScanOptions options)方法获取匹配键值对记录为空");
return;
}
while (cursor.hasNext()) {
// 遍历缓存
Map.Entry<Object, Object> entry = cursor.next();
String key = String.valueOf(entry.getKey());
}
} catch (Exception ex) {
logger.info(ex.toString());
}
关于 try-with-resources用法需要提一点的就是,resources对象必须是实现了 java.lang.AutoCloseable接口,才会自动关闭对象。
补充知识:redis连接未释放,导致redis连接池满,从而应用服务不可用的问题定位和解决
版本提交测试验收后,跑了几天,今天测试突然跑来说平台不可用。
1. 我先是试图登录平台,发现首页可以进入,但是登录不成功。很显然是后台的问题。
2. 再看MQ中,发现消息堆积在队列中,未被消费掉,同时一点一点变化,说明很有可能是哪里有内存或连接的泄露或未释放。
3. 接着登录阿里云账号,查看redis监控,发现连接数已经达到9000多。
4. 查看日志发现大量的redis连接拒绝错误
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. 当然后台的tomcat中也报了其他错误,比如:
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. 但是,还是继续查看日志,发现一直有个 [TaskId]PUPHVUNVJSSMKOTQPKHRSPOMUKKOKLPG [Message]null [Result]0 debug日志,因为只有一直不停的发,且连接不关闭才可能出现这么多连接的情况。一边对应代码,发现是页面上调用后台代码,发送给设备长连接的代码。
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)这条语句就未执行。然后进一步怀疑页面是否是定时去获取,通过F12,发现每10s钟请求一次,页面上要获取设备的上下行速率。所以会累积这么多的请求。最后,修改也比较简单,添加finally块,保证RedisUtils.return(jedis)必定会执行。
8. 接下来在另一个开发环境继续复现,我们将redisManagerPool中设置maxTotal=300,maxIdle=30。而原先是3000、300,这样有利于快速复现。
果然,一上午时间就达到了300的限制。出现了一样的问题。
9. 综上,问题定位清楚,且修复该问题。
a) 对于oss redis之类的第三方网络连接,必须要有finally块执行。否则后续很容易由于不规范的编码,出现这种连接未正常释放的问题。
b) 定位问题,还是需要有日志。如果单从代码去查,方向会比较多且很容易浪费时间。
c) 修改池大小,缩短复现时间,快速定位修改。
来源:https://blog.csdn.net/loongshawn/article/details/109071791


猜你喜欢
- (效果如上图所示)其实很简单:比方说上面的容器是一个ListView<ListView android:id="@+id/l
- 1.创建动画控制器,双击打开动画控制器,创建 状态并添加动画片段,并且状态与状态之间进行连线,往返的都要有,在Animator的左上角–Pa
- 前言最近公司在重构广告系统,其中核心的打包功由广告系统调用,即对apk打包的调用和打包完成之后的回调,需要提供相应的接口给广告系统。因此,为
- 现在越来越多手机支持OTG功能,通过OTG可以实现与外接入的U盘等USB设备实现数据传输。 USB OTG(On The Go)作
- java 实现文件夹的拷贝实例代码 这里就直接上代码,
- 一、运算符运算符包括下面几种:算术运算符赋值运算符比较运算符逻辑运算符位运算符三目运算符最不常用的是位运算符,但也是最接近计算机底层的。1、
- 情景描述将一个时间转换为对应的unix时间戳,字符集使用UTF-8编码,数据通讯统一采用 HTTP 协议通讯,使用POST 方法请求并传递参
- 题目要求思路一:枚举 + 二分逐一枚举值域内的所有值,然后二分判断是否合法。Javaclass Solution { &nbs
- 本文介绍了Android EasyBarrage实现轻量级弹幕效果,分享给大家,具体如下:概述EasyBarrage是Android平台的一
- 一、首先下载 ZXing.Net地址是:http://zxingnet.codeplex.com/releases/view/117068然
- 一、读线圈状态/// <summary> /// 读线圈状态测试 &nbs
- 概述企业内部一般都有一套单点登录系统(常用的实现有apereo cas),所有的内部系统的登录认证都对接它。本文介绍spring boot的
- Java Boolean 初始化方式1、Boolean(String boolString);以字符串的方式初始化,只有当字符串是“true
- 1 前言为什么我们在使用SpringBoot框架开发Java Web应用需要引入大量的starter?例如,我们引入Redis就在Maven
- SpringBoot自带Tomcat,所以我们的项目可以单独部署,不需要依赖Window、Linux系统中的服务器,所以打包出来的Jar包是
- 当jvm虚拟机被关闭的时候,可能我们需要做一些处理,比如对连接的关闭,或者对一些必要信息的存储等等操作,这里就可以借助于虚拟机提供的钩子函数
- 前言首先,啊,先简单介绍一下优先队列的概念,学数据结构以及出入算法竞赛的相信都对队列这一数据结构十分熟悉,这是一个线性的数据结构.针对队列这
- RecyclerView 滑动时的优化处理,在滑动时停止加载图片,在滑动停止时开始加载图片,这里用了Glide.pause 和Glide.r
- 不久前项目开始了一段时间了,刚开始怀疑是Android Studio中新加入的Instant Run功能引起的,于是重新打release包后
- 最近项目中要做一个带进度条的上传文件的功能,学习了AsyncTask,使用起来比较方便,将几个方法实现就行,另外做了一个很简单的demo,希