在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指令的更多相关文章
- redis中使用SCAN代替KEYS
		前言 由于redis的keys命令是线上禁用,所以就有了SCAN.SSCAN.HSCAN和ZSCAN四个命令. 但是这四个命令也不是每次返回全部匹配结果,因此需要一遍遍执行下去,而且每次返回的curs ... 
- redis中scan和keys的区别
		scan和keys的区别 redis的keys命令,通来在用来删除相关的key时使用,但这个命令有一个弊端,在redis拥有数百万及以上的keys的时候,会执行的比较慢,更为致命的是,这个命令会阻塞r ... 
- Oracle 11G R2 RAC中的scan ip 的用途和基本原理【转】
		Oracle 11G R2 RAC增加了scan ip功能,在11.2之前,client链接数据库的时候要用vip,假如你的cluster有4个节点,那么客户端的tnsnames.ora中就对应有四个 ... 
- freemarker中8个常用的指令
		这里列举出Freemarker模板文件中8个常用的指令. 1. assign assign指令用于创建或替换一个顶层变量,assign指令的用法有多种,包括创建或替换一个顶层变量,创建或替换多个变量等 ... 
- 【Azure Redis 缓存】Azure Cache for Redis 中如何快速查看慢指令情况(Slowlogs)
		问题描述 当 Azure Redis 服务器负载过高的情况下,使用时就会遇见连接超时,命令超时,IO Socket超时等异常.为了能定位是那些因素引起的,可以参考微软官方文档( 管理 Azure Ca ... 
- Redis中的Scan命令的使用
		Redis中有一个经典的问题,在巨大的数据量的情况下,做类似于查找符合某种规则的Key的信息,这里就有两种方式,一是keys命令,简单粗暴,由于Redis单线程这一特性,keys命令是以阻塞的方式执行 ... 
- RedisTemplate 中 opsForHash()使用  (没有测试过,copy的)
		1.put(H key, HK hashKey, HV value) //新增hashMap值 redisTemplate.opsForHash().put("hashValue" ... 
- redis 用scan 代替keys,hgetAll
		转载自:https://blog.csdn.net/w05980598/article/details/80264568 众所周知,当redis中key数量越大,keys 命令执行越慢,而且最重要的会 ... 
- Redis中的Scan命令踩坑记
		1 原本以为自己对redis命令还蛮熟悉的,各种数据模型各种基于redis的骚操作.但是最近在使用redis的scan的命令式却踩了一个坑,顿时发觉自己原来对redis的游标理解的很有限.所以记录下这 ... 
随机推荐
- js的6种继承方式
			重新理解js的6种继承方式 注:本文引用于http://www.cnblogs.com/ayqy/p/4471638.html 重点看第三点 组合继承(最常用) 写在前面 一直不喜欢JS的OOP,在学 ... 
- XSS漏洞初窥(通过dvwa平台进测试)
			xss的全称是:Cross Site Script,中文名叫“跨站脚本攻击”,因为和CSS重名,所以改名XSS.作为一个网站是肯定要和用户有交互的,那么肯定就伴随着信息的输入输出,而利用xss就是通过 ... 
- Java进程间通信学习
			转自:https://www.iteye.com/blog/polim-1278435 进程间通信的主要方法有:(1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共 ... 
- Node.js官方文档:到底什么是阻塞(Blocking)与非阻塞(Non-Blocking)?
			译者按: Node.js文档阅读系列之一. 原文: Overview of Blocking vs Non-Blocking 译者: Fundebug 为了保证可读性,本文采用意译而非直译. 这篇博客 ... 
- Using hints for Postgresql
			本文转自:http://pghintplan.osdn.jp/pg_hint_plan.html pg_hint_plan 1.1 pg_hint_plan Name Synopsis Descrip ... 
- Livy 安装教程
			Livy 安装教程 本文原始地址:https://sitoi.cn/posts/16143.html 安装环境 Fedora 29 Spark PySpark 安装步骤 下载 Livy 安装包 解压 ... 
- Leetcode——3. 无重复字符的最长子串
			难度: 中等 题目 Given a string, find the length of the longest substring without repeating characters. 给定一 ... 
- node  单线程异步非阻塞
			链接:http://www.runoob.com/nodejs/nodejs-callback.html 首先什么是单线程异步非阻塞? 单线程的意思整个程序从头到尾但是运用一个线程,程序是从上往下执行 ... 
- luoguP4197:Peaks(Kruskal重构树+主席树)或者(点分树+离线)
			题意:有N座山,M条道路.山有山高,路有困难值(即点权和边权).现在Q次询问,每次给出(v,p),让求从v出发,只能结果边权<=p的边,问能够到达的山中,第K高的高度(从大到小排序). 思路:显 ... 
- Windows:安装Python2.7、3.6与共存,安装pycharm
			Windows:安装Python2.7.3.6与共存,安装pycharm 目录: 1.下载Python2.7.Python3.6 2.安装Python2.7 3.安装Python3.6 4.安装破解p ... 
