Spring RedisTemplate实现scan操作,毕竟keys不安全 您所在的位置:网站首页 redistemplate模糊匹配key Spring RedisTemplate实现scan操作,毕竟keys不安全

Spring RedisTemplate实现scan操作,毕竟keys不安全

2023-03-25 13:06| 来源: 网络整理| 查看: 265

先了解下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 也可以使用 (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()) { Map.Entry entry = cursor.next(); Object key = entry.getKey(); Object valueSet = entry.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, boolean, boolean) finally { RedisConnectionUtils.releaseConnection(conn, factory); } 代码虽然只是调用一次scan方法,但是spring-data-redis已经对scan做了封装,这个scan结合cursor.hasNext会多次redis scan,最终拿到所有match的结果 2. scan 2.1 使用spring-data-redis封装好的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 使用redis.clients.jedis的MultiKeyCommands,自己循环scan 获取 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); // 这个不是返回结果的数量,应该是每次scan的数量 ScanResult scan = multiKeyCommands.scan("0", scanParams); while (null != scan.getStringCursor()) { keys.addAll(scan.getResult()); // 这一次scan match到的结果 if (!StringUtils.equals("0", scan.getStringCursor())) { // 不断拿着新的cursor scan,最终会拿到所有匹配的值 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 (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()); Standalone Sentinel Cluster区别

待更新

参考

redistemplate-游标scan使用注意事项

如何使用RedisTemplate访问Redis数据结构

Redis 中 Keys 与 Scan 的使用

深入理解Redis的scan命令

spring-boot-starter-redis配置详解

线上大量CLOSE_WAIT原因排查

redis如何配置standAlone版的jedisPool

一次jedis使用不规范,导致redis客户端close_wait大量增加的bug



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有