redis scan命令导致redis连接耗尽,线程上锁的解决

使用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) 修改池大小,缩短复现时间,快速定位修改。

以上这篇redis scan命令导致redis连接耗尽,线程上锁的解决就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Redis分布式锁的使用和实现原理详解

    模拟一个电商里面下单减库存的场景. 1.首先在redis里加入商品库存数量. 2.新建一个Spring Boot项目,在pom里面引入相关的依赖. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <gr

  • Redis客户端及服务端的安装教程详解

    本系列将和大家分享Redis分布式缓存,本文是该系列的开篇,主要简单介绍下Redis客户端及服务端的安装. 一.Redis简介 Redis:Remote Dictionary Server 远程字典服务器 基于内存管理(数据存在内存),实现了5种数据结构(分别应对各种具体需求),单线程模型的应用程序(单进程单线程),对外提供插入--查询--固化--集群功能. 正是因为基于内存管理所以速度快,可以用来提升性能.但是不能当数据库,不能作为数据的最终依据. 单线程多进程的模式来提供集群服务. 单线程最

  • redission分布式锁防止重复初始化问题

    配置地址: redisson: # Redis服务地址 如果集群使用","进行分割 server-address: redis://${spring.redis.host}:${spring.redis.port} database: ${spring.redis.database} 创建配置类: @ConfigurationProperties(prefix = "redisson") @Configuration public class RedissonCon

  • redis scan命令导致redis连接耗尽,线程上锁的解决

    使用redis scan方法无法获取connection,导致线程锁死. 0.关键字 redis springboot redistemplate scan try-with-resource 1.异常现象 应用部署后,功能正常使用,但约数小时左右,部分功能接口异常,接口请求无响应. 2.异常排查 查看堆栈信息,jstask pid.首先找到java进程pid:输出堆栈信息至log文件,jstask 30 > stask.log,看到与redis相关的日志,线程状态为waiting. "p

  • 详解Redis SCAN命令实现有限保证的原理

    SCAN命令可以为用户保证:从完整遍历开始直到完整遍历结束期间,一直存在于数据集内的所有元素都会被完整遍历返回,但是同一个元素可能会被返回多次.如果一个元素是在迭代过程中被添加到数据集的,又或者是在迭代过程中从数据集中被删除的,那么这个元素可能会被返回,也可能不会返回. 这是如何实现的呢,先从Redis中的字典dict开始.Redis的数据库是使用dict作为底层实现的. 字典数据类型 Redis中的字典由dict.h/dict结构表示: typedef struct dict { dictTy

  • Redis SCAN命令详解

    目录 1. 获取指定前缀的key 需求描述: 解决方案: 2. SCAN命令 Redis Scan 命令用于迭代数据库中的数据库键. SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程. SCAN 返回一个包含两个元素的数组, 第一个元素是用于进行下一次迭代的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素.如果新游标返回 0 表示迭代已结束. 相

  • Redis Scan命令的基本使用方法

    1. 概述 SCAN 命令以及比较相近的 SSCAN.HSCAN 和 ZSCAN 命令都用于增量迭代数据集元素: SCAN 命令用于迭代当前数据库中的数据库键. SSCAN 命令用于迭代集合(Set)中的元素. HSCAN 命令用于迭代哈希(Hash)中的字段以及对应的值. ZSCAN 命令用于迭代有序集合(Sorted Set)中的元素以及对应的得分. 由于这些命令都可以增量迭代,每次调用都只会返回少量元素,所以这些命令可以用于生产环境中,不用担心像使用 KEYS.SMEMBERS 命令带来的

  • Redis中Scan命令的踩坑实录

    1.原本以为自己对redis命令还蛮熟悉的,各种数据模型各种基于redis的骚操作.但是最近在使用redis的scan的命令式却踩了一个坑,顿时发觉自己原来对redis的游标理解的很有限.所以记录下这个踩坑的过程,背景如下: 公司因为redis服务器内存吃紧,需要删除一些无用的没有设置过期时间的key.大概有500多w的key.虽然key的数目听起来挺吓人.但是自己玩redis也有年头了,这种事还不是手到擒来? 当时想了下,具体方案是通过lua脚本来过滤出500w的key.然后进行删除动作.lu

  • 关于Redis bigkeys命令会阻塞问题的解决

    目录 前言 一. 顺丰高级开发工程师在线执行了 Redis 危险命令导致某公司损失 400 万 二.测试一下1000万数据的性能 1.编写脚本文件 2.写入Redis1000万数据 3.通过keys * 查看1000万数据 4.通过配置文件禁止keys *的使用 三.使用scan替代keys * 四.拒绝bigkey 1.阿里云Redis开发规范 2.出现bigkey时如何删除? 3.bigkey会造成哪些问题? 4.如何发现bigkey? 前言 今天分享一次Redis引发的线上事故,避免再次踩

  • Redis keys命令的具体使用

    keys命令: DEL KEY:该命令用于在key存在时删除key DUMP KEY:序列化给定key,并返回被序列化的值 序列化:把对象转化为可传输的字节的序列过程称为序列化 反序列化:把字节序列还原为对象的过程称为反序列化 为什么需要序列化? 序列化的最终目的是为了对象可以跨平台传输,和进行网络传输.而我们进行跨平台存储和网络传输的方式就是IO,而IO支持的数据格式就是字节数组. 因为我们单方面的只把对象转成字节数组还不行,因为没有规则的字节数组我们是没办法把对象的本来面目还原回来的,所以我

  • Redis通用命令介绍以及key的层级结构讲解

    目录 1 Redis数据结构介绍 2 Redis通用命令 3 Redis命令-Key的层级结构 1 Redis数据结构介绍 Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样: value的数据类型共有8种,前面5中为基本数据类型,后面3种是针对不同的情况指定的特殊数据类型. 命令不要死记,学会查询就好啦 Redis为了方便我们学习,将操作不同数据类型的命令也做了分组,在官网( Commands | Redis)可以查看到不同的命令:(点击CO

  • python使用redis模块来跟redis实现交互

    目录 redis模块的使用 1.安装模块 2.导入模块 4.连接池 5.操作 管道 事务 订阅\发布 redis模块的使用 1.安装模块 pip3 install redis 2.导入模块 import redis 3.连接方式 严格连接模式:r=redis.StrictRedis(host=“”,port=) 更Python化的连接模式:r=redis.Redis(host=“”,port=) StrictRedis用于实现大部分官方的命令,并使用官方的语法和命令 Redis与StrictRe

  • Redis中scan命令的深入讲解

    前言 熟悉Redis的人都知道,它是单线程的.因此在使用一些时间复杂度为O(N)的命令时要非常谨慎.可能一不小心就会阻塞进程,导致Redis出现卡顿. 有时,我们需要针对符合条件的一部分命令进行操作,比如删除以test_开头的key.那么怎么获取到这些key呢?在Redis2.8版本之前,我们可以使用keys命令按照正则匹配得到我们需要的key.但是这个命令有两个缺点: 没有limit,我们只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是"无穷无尽"的字符串输出

随机推荐