共计 9551 个字符,预计需要花费 24 分钟才能阅读完成。
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 使用注意事项
如何使用 RedisTemplate 访问 Redis 数据结构
Redis 中 Keys 与 Scan 的使用
深入理解 Redis 的 scan 命令
spring-boot-starter-redis 配置详解
线上大量 CLOSE_WAIT 原因排查
redis 如何配置 standAlone 版的 jedisPool
一次 jedis 使用不规范,导致 redis 客户端 close_wait 大量增加的 bug
正文完
发表至: java
2019-08-30