zoukankan      html  css  js  c++  java
  • 在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.getNativeConnectionconnection.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. 修改后配置

      1. 配置方式一:部分接口已经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;
    }
    
      1. 配置方式二:这是群里好友给找的新的配置方式,效果一样
    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

  • 相关阅读:
    #leetcode687.最长同值路径
    #leetcode404.所有左叶子节点之和
    #leetcode111.二叉树的最小深度
    #leetcode101.对称二叉树
    #leetcode437.路径总和II
    #leetcode112.路径总和
    #leetcode543.二叉树的直径
    #leetcode110.平衡二叉树
    springboot数据库密码加密-使用自定义加密算法
    PHP加密
  • 原文地址:https://www.cnblogs.com/alterem/p/11433340.html
Copyright © 2011-2022 走看看