在RedisTemplate中使用scan代替keys指令操作

keys * 这个命令千万别在生产环境乱用。特别是数据庞大的情况下。因为Keys会引发Redis锁,并且增加Redis的CPU占用。很多公司的运维都是禁止了这个命令的

当需要扫描key,匹配出自己需要的key时,可以使用 scan 命令

scan操作的Helper实现

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisHelper {

 @Autowired
 private StringRedisTemplate stringRedisTemplate;

 /**
 * scan 实现
 * @param pattern 表达式
 * @param consumer 对迭代到的key进行操作
 */
 public void scan(String pattern, Consumer<byte[]> consumer) {
 this.stringRedisTemplate.execute((RedisConnection connection) -> {
 try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(Long.MAX_VALUE).match(pattern).build())) {
 cursor.forEachRemaining(consumer);
 return null;
 } catch (IOException e) {
 e.printStackTrace();
 throw new RuntimeException(e);
 }
 });
 }

 /**
 * 获取符合条件的key
 * @param pattern 表达式
 * @return
 */
 public List<String> keys(String pattern) {
 List<String> keys = new ArrayList<>();
 this.scan(pattern, item -> {
 //符合条件的key
 String key = new String(item,StandardCharsets.UTF_8);
 keys.add(key);
 });
 return keys;
 }
}

但是会有一个问题:没法移动cursor,也只能scan一次,并且容易导致redis链接报错

先了解下scan、hscan、sscan、zscan

http://doc.redisfans.com/key/scan.html

keys 为啥不安全?

keys的操作会导致数据库暂时被锁住,其他的请求都会被堵塞;业务量大的时候会出问题

Spring RedisTemplate实现scan

1. hscan sscan zscan

例子中的"field"是值redis的key,即从key为"field"中的hash中查找

redisTemplate的opsForHash,opsForSet,opsForZSet 可以 分别对应 sscan、hscan、zscan

当然这个网上的例子其实也不对,因为没有拿着cursor遍历,只scan查了一次

可以偷懒使用 .count(Integer.MAX_VALUE),一下子全查回来;但是这样子和 keys 有啥区别呢?搞笑脸 & 疑问脸

可以使用 (JedisCommands) connection.getNativeConnection()的 hscan、sscan、zscan 方法实现cursor遍历,参照下文2.2章节

try {
 Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash().scan("field",
 ScanOptions.scanOptions().match("*").count(1000).build());
 while (cursor.hasNext()) {
  Object key = cursor.next().getKey();
  Object valueSet = cursor.next().getValue();
 }
 //关闭cursor
 cursor.close();
} catch (IOException e) {
 e.printStackTrace();
}

cursor.close(); 游标一定要关闭,不然连接会一直增长;可以使用client lists``info clients``info stats命令查看客户端连接状态,会发现scan操作一直存在

我们平时使用的redisTemplate.execute 是会主动释放连接的,可以查看源码确认

client list
......
id=1531156 addr=xxx:55845 fd=8 name= age=80 idle=11 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=scan
......
org.springframework.data.redis.core.RedisTemplate#execute(org.springframework.data.redis.core.RedisCallback<T>, boolean, boolean)

finally {
 RedisConnectionUtils.releaseConnection(conn, factory);
}

2. scan

2.1 网上给的例子多半是这个

这个 connection.scan 没法移动cursor,也只能scan一次

public Set<String> scan(String matchKey) {
 Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
  Set<String> keysTmp = new HashSet<>();
  Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("*" + matchKey + "*").count(1000).build());
  while (cursor.hasNext()) {
   keysTmp.add(new String(cursor.next()));
  }
  return keysTmp;
 });

 return keys;
}

2.2 使用 MultiKeyCommands

获取 connection.getNativeConnection;connection.getNativeConnection()实际对象是Jedis(debug可以看出) ,Jedis实现了很多接口

public class Jedis extends BinaryJedis implements JedisCommands, MultiKeyCommands, AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands

当 scan.getStringCursor() 存在 且不是 0 的时候,一直移动游标获取

public Set<String> scan(String key) {
 return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
  Set<String> keys = Sets.newHashSet();

  JedisCommands commands = (JedisCommands) connection.getNativeConnection();
  MultiKeyCommands multiKeyCommands = (MultiKeyCommands) commands;

  ScanParams scanParams = new ScanParams();
  scanParams.match("*" + key + "*");
  scanParams.count(1000);
  ScanResult<String> scan = multiKeyCommands.scan("0", scanParams);
  while (null != scan.getStringCursor()) {
   keys.addAll(scan.getResult());
   if (!StringUtils.equals("0", scan.getStringCursor())) {
    scan = multiKeyCommands.scan(scan.getStringCursor(), scanParams);
    continue;
   } else {
    break;
   }
  }

  return keys;
 });
}

发散思考

cursor没有close,到底谁阻塞了,是 Redis 么

测试过程中,我基本只要发起十来个scan操作,没有关闭cursor,接下来的请求都卡住了

redis侧分析

client lists``info clients``info stats查看

发现 连接数 只有 十几个,也没有阻塞和被拒绝的连接

config get maxclients查询redis允许的最大连接数 是 10000

1) "maxclients"

2) "10000"`

redis-cli在其他机器上也可以直接登录 操作

综上,redis本身没有卡死

应用侧分析

netstat查看和redis的连接,6333是redis端口;连接一直存在

➜ ~ netstat -an | grep 6333
netstat -an | grep 6333
tcp4  0  0 xx.xx.xx.aa.52981  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52979  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52976  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52971  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52969  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52967  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52964  xx.xx.xx.bb.6333  ESTABLISHED
tcp4  0  0 xx.xx.xx.aa.52961  xx.xx.xx.bb.6333  ESTABLISHED

jstack查看应用的堆栈信息

发现很多 WAITING 的 线程,全都是在获取redis连接

所以基本可以断定是应用的redis线程池满了

"http-nio-7007-exec-2" #139 daemon prio=5 os_prio=31 tid=0x00007fda36c1c000 nid=0xdd03 waiting on condition [0x00007000171ff000]
 java.lang.Thread.State: WAITING (parking)
  at sun.misc.Unsafe.park(Native Method)
  - parking to wait for <0x00000006c26ef560> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
  at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
  at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
  at org.apache.commons.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:590)
  at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:441)
  at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:362)
  at redis.clients.util.Pool.getResource(Pool.java:49)
  at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
  at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16)
  at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:276)
  at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:469)
  at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:132)
  at org.springframework.data.redis.core.RedisTemplate.executeWithStickyConnection(RedisTemplate.java:371)
  at org.springframework.data.redis.core.DefaultHashOperations.scan(DefaultHashOperations.java:244)

综上,是应用侧卡死

后续

过了一个中午,redis client lists显示 scan 连接还在,没有释放;应用线程也还是处于卡死状态

检查 config get timeout,redis未设置超时时间,可以用 config set timeout xxx设置,单位秒;但是设置了redis的超时,redis释放了连接,应用还是一样卡住

1) "timeout"

2) "0"

netstat查看和redis的连接,6333是redis端口;连接从ESTABLISHED变成了CLOSE_WAIT;

jstack和 原来表现一样,卡在JedisConnectionFactory.getConnection

➜ ~ netstat -an | grep 6333
netstat -an | grep 6333
tcp4  0  0 xx.xx.xx.aa.52981  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52979  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52976  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52971  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52969  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52967  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52964  xx.xx.xx.bb.6333  CLOSE_WAIT
tcp4  0  0 xx.xx.xx.aa.52961  xx.xx.xx.bb.6333  CLOSE_WAIT

回顾一下TCP四次挥手

ESTABLISHED 表示连接已被建立

CLOSE_WAIT 表示远程计算器关闭连接,正在等待socket连接的关闭

和现象符合

redis连接池配置

根据上面 netstat -an基本可以确定 redis 连接池的大小是 8 ;结合代码配置,没有指定的话,默认也确实是8

redis.clients.jedis.JedisPoolConfig
private int maxTotal = 8;
private int maxIdle = 8;
private int minIdle = 0;

如何配置更大的连接池呢?

A. 原配置

@Bean
public RedisConnectionFactory redisConnectionFactory() {
 RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
 redisStandaloneConfiguration.setHostName(redisHost);
 redisStandaloneConfiguration.setPort(redisPort);
 redisStandaloneConfiguration.setPassword(RedisPassword.of(redisPasswd));
 JedisConnectionFactory cf = new JedisConnectionFactory(redisStandaloneConfiguration);
 cf.afterPropertiesSet();
 return cf;
}
readTimeout,connectTimeout不指定,有默认值 2000 ms

org.springframework.data.redis.connection.jedis.JedisConnectionFactory.MutableJedisClientConfiguration
private Duration readTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
private Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);

B. 修改后配置

配置方式一:部分接口已经Deprecated了

@Bean
public RedisConnectionFactory redisConnectionFactory() {
 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
 jedisPoolConfig.setMaxTotal(16); // --最多可以建立16个连接了
 jedisPoolConfig.setMaxWaitMillis(10000); // --10s获取不到连接池的连接,
            // --直接报错Could not get a resource from the pool

 jedisPoolConfig.setMaxIdle(16);
 jedisPoolConfig.setMinIdle(0);

 JedisConnectionFactory cf = new JedisConnectionFactory(jedisPoolConfig);
 cf.setHostName(redisHost); // -- @Deprecated
 cf.setPort(redisPort); // -- @Deprecated
 cf.setPassword(redisPasswd); // -- @Deprecated
 cf.setTimeout(30000); // -- @Deprecated 貌似没生效,30s超时,没有关闭连接池的连接;
       // --redis没有设置超时,会一直ESTABLISHED;redis设置了超时,且超时之后,会一直CLOSE_WAIT

 cf.afterPropertiesSet();
 return cf;
}

配置方式二:这是群里好友给找的新的配置方式,效果一样

RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(redisHost);
redisStandaloneConfiguration.setPort(redisPort);
redisStandaloneConfiguration.setPassword(RedisPassword.of(redisPasswd));

JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(16);
jedisPoolConfig.setMaxWaitMillis(10000);
jedisPoolConfig.setMaxIdle(16);
jedisPoolConfig.setMinIdle(0);

cf = new JedisConnectionFactory(redisStandaloneConfiguration, JedisClientConfiguration.builder()
  .readTimeout(Duration.ofSeconds(30))
  .connectTimeout(Duration.ofSeconds(30))
  .usePooling().poolConfig(jedisPoolConfig).build());

以上这篇在RedisTemplate中使用scan代替keys指令操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Redis命令使用技巧之Keys的相关操作

    前言 介绍完Redis连接相关命令后,再来介绍一下与Key相关的命令,Redis作为一个key-value数据库,对Key进行操作是无法避免的. KEYS 的速度非常快,但在一个大的数据库中使用它仍然可能造成性能问题,如果你需要从一个数据集中查找特定的 key ,你最好还是用 Redis 的集合结构(set)来代替. DEL 最早可用版本1.0.0 删除指定的键值对,如果指定的key不存在,则忽略.DEL命令的时间复杂度是O(N),对于除字符串外的其他数据类型,命令的时间复杂度为O(M),M是值

  • redis 用scan指令 代替keys指令(详解)

    众所周知,当redis中key数量越大,keys 命令执行越慢,而且最重要的会阻塞服务器,对单线程的redis来说,简直是灾难,终于找到了替代命令scan. SCAN cursor [MATCH pattern] [COUNT count] SCAN 命令及其相关的 SSCAN 命令. HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements): SCAN 命令用于迭代当前数据库中的数据库键. S

  • Redis的KEYS 命令千万不能乱用

    KESY 命令 时间复杂度: O(N) , 假设Redis中的键名和给定的模式的长度有限的情况下,N为数据库中key的个数. Redis Keys 命令用于查找所有符合给定模式 pattern 的 key 尽管这个操作的时间复杂度是 O(N), 但是常量时间相当低.例如,在一个普通笔记本上跑Redis,扫描100万个key只要40毫秒. 命令格式 KEYS pattern Warning: 生产环境使用 KEYS 命令需要非常小心.在大的数据库上执行命令会影响性能.这个命令适合用来调试和特殊操作

  • Redis 不使用 keys 命令获取键值信息的方法

    1. 问题来源 这个问题可能看起来很奇怪,但很多 redis 集群会有一个统一的入口,入口会作兼容 redis 命令的代理,一般出于新能考虑是禁止使用 keys 命令来获取键值信息的,但是可以通过 scan 命令来代替 keys 2. 使用 keys 的方法 127.0.0.1:6379> KEYS * 1) "_kombu.binding.test_queue" 2) "a8e620b9-e52e-3498-8a1c-448f35783058" 3) &qu

  • 在RedisTemplate中使用scan代替keys指令操作

    keys * 这个命令千万别在生产环境乱用.特别是数据庞大的情况下.因为Keys会引发Redis锁,并且增加Redis的CPU占用.很多公司的运维都是禁止了这个命令的 当需要扫描key,匹配出自己需要的key时,可以使用 scan 命令 scan操作的Helper实现 import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.

  • Redis中键和数据库通用指令详解

    目录 一.Redis键(key)通用指令 1.key基本操作 2.时效性控制 3.查询模式 4.其它操作 二.数据库通用指令 1.基本操作 2.相关操作 一.Redis键(key)通用指令 可以参考菜鸟教程:Redis 键命令用于管理 redis 的键 key特征:key是一个字符串,通过key获取redis中保存的数据. 1.key基本操作 命令 功能 del key 该命令用于在 key 存在时删除 key exists key 检查给定 key 是否存在 type key 返回 key 所

  • Angularjs中使用轮播图指令swiper

    我们在angualrjs移动开发中遇到轮播图的功能 安装 swiper  npm install --save swiper   或者 bower install --save swiper 引入文件路径 <link rel="stylesheet" href="../bower_components/swiper/dist/css/swiper.min.css" rel="external nofollow" /> <scri

  • angular.js指令中transclude选项及ng-transclude指令详解

    前言 在开始本文之前,首先要说明我们使用的angular的版本是1.5.0,因为不同版本的表现结果不是那么相同. 首先我们应该了解到,在angular指令的选项中,有一项是transclude,这个选项有三种值:false,true,object:那这三种值分别表示什么,该如何选择? 下面我们来详细的说明一下. transclude字面意思就是嵌入,也就是说你需不需要将你的指令内部的元素(注意不是指令的模板)嵌入到你的模板中去,默认是false.如果你需要这种功能的话,那么就需要将transcl

  • 关于在php.ini中添加extension=php_mysqli.dll指令的说明

    在配置php5时要使用mysql作为数据库,很多人都认为只要在php.ini中添加extension=php_mysql.dll;指令即可,不清楚为什么很多文章都推荐还要添加extension=php_mysqli.dll;指令. 只要查看官方最新php手册便知,上面写到: 下面是内置的扩展库列表: PHP 5 中(截止到 5.0.4)有以下修改.新增内置:DOM,LibXML,Iconv,SimpleXML,SPL 和SQLite.以下不再内置:MySQL 和 Overload. 原来php5

  • 浅谈linux中的whoami与 who指令

    whoami 功能说明: 显示用户名称 语法: whoami 补充说明: 显示自身的用户名称,本指令相当于执行  id -un 指令 whoami 与 who am i的区别 who这个命令重点在用来查看当前有那些用户登录到了本台机器上 who -m的作用和who am i的作用是一样的 who am i显示的是实际用户的用户名,即用户登陆的时候的用户ID.此命令相当于who -m whoami显示的是有效用户ID ,是当前操作用户的用户名 命令实践: [test@test~]$ whoami 

  • 深入浅析Vue.js 中的 v-for 列表渲染指令

    1 基本用法 当遍历一个数组或枚举一个对象进行迭代循环展示时,就会用到列表渲染指令 v-for. 它的表达式需要结合 in 来使用,类似 item in items 的形式. 1.1 遍历数组 html: <div id="app"> <ul> <li v-for="n in news">{{n.title}}</li> </ul> </div> js: <script> var a

  • 浅谈在Vue.js中如何实现时间转换指令

    在社区中,发布的动态信息,经常会有一个相对余实际发布时间的相对时间.比如这里的微博: 服务端存储的时间格式,一般为 Unix 时间戳,比如 2019/1/6 13:40:1 的Unix 时间戳为 1546753201651.前端在获取到这个时间戳之后,会转换为可读格式的时间.在社交类产品中,一般会将时间戳转换为 x 分钟前,x 小时前或者 x 天前,因为这样的显示方式用户体验更好. 我们可以自定义一个 v-relative-time 指令来实现上述功能. html: <!DOCTYPE html

  • 对angular2中的ngfor和ngif指令嵌套实例讲解

    ng2 结构指令不能直接嵌套使用,可使用标签来包裹指令 示例如下 <ul> <ng-container *ngFor="let item of lists"> <div class="thumb p-date" *ngIf="item.picurl"> <a href="# " rel="external nofollow" ><img src=&quo

随机推荐