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

网友投稿 1988 2023-03-08

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

在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 consumer) {

this.stringRedisTemplate.execute((RedisConnection connection) -> {

try (Cursor 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 keys(String pattern) {

List 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> 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 dbqfyTWlvEZj=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, boolean, boolean)

finally {

RedisConnectionUtils.releaseConnection(conn, factory);

}

2. scan

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

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

public Set scan(String matchKey) {

Set keys = redisTemplate.execute((RedisCallback>) connection -> {

Set keysTmp = new HashSet<>();

Cursor 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 scan(String key) {

return redisTemplate.execute((RedisCallback>) connection -> {

Set keys = Sets.newHashSet();

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

MultiKeyCommands multiKeyCommands = (MultiKeyCommands) commands;

ScanParams scanParams = new ScanParams();

scanParams.match("*" + key + "*");

scanParams.count(1000);

ScanResult 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(AbstrqfyTWlvEZjactQueuedSynchronizer.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.http://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());

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:微信电脑版怎么打开小程序(电脑版微信怎么多开)
下一篇:微信小程序app下载(微信小程序下载)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~