keys * 这个命令千万别在生产环境乱用。特别是数据庞大的情况下。因为Keys会引发Redis锁,并且增加Redis的CPU占用。很多公司的运维都是禁止了这个命令的

当需要扫描key,匹配出自己需要的key时,可以使用 scan 命令

scan操作的Helper实现

  1. import java.io.IOException;
  2. import java.nio.charset.StandardCharsets;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import java.util.function.Consumer;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.data.redis.connection.RedisConnection;
  8. import org.springframework.data.redis.core.Cursor;
  9. import org.springframework.data.redis.core.ScanOptions;
  10. import org.springframework.data.redis.core.StringRedisTemplate;
  11. import org.springframework.stereotype.Component;
  12. @Component
  13. public class RedisHelper {
  14. @Autowired
  15. private StringRedisTemplate stringRedisTemplate;
  16. /**
  17. * scan 实现
  18. * @param pattern 表达式
  19. * @param consumer 对迭代到的key进行操作
  20. */
  21. public void scan(String pattern, Consumer<byte[]> consumer) {
  22. this.stringRedisTemplate.execute((RedisConnection connection) -> {
  23. try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().count(Long.MAX_VALUE).match(pattern).build())) {
  24. cursor.forEachRemaining(consumer);
  25. return null;
  26. } catch (IOException e) {
  27. e.printStackTrace();
  28. throw new RuntimeException(e);
  29. }
  30. });
  31. }
  32. /**
  33. * 获取符合条件的key
  34. * @param pattern 表达式
  35. * @return
  36. */
  37. public List<String> keys(String pattern) {
  38. List<String> keys = new ArrayList<>();
  39. this.scan(pattern, item -> {
  40. //符合条件的key
  41. String key = new String(item,StandardCharsets.UTF_8);
  42. keys.add(key);
  43. });
  44. return keys;
  45. }
  46. }

但是会有一个问题:没法移动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章节
  1. try {
  2. Cursor<Map.Entry<Object,Object>> cursor = redisTemplate.opsForHash().scan("field",
  3. ScanOptions.scanOptions().match("*").count(1000).build());
  4. while (cursor.hasNext()) {
  5. Object key = cursor.next().getKey();
  6. Object valueSet = cursor.next().getValue();
  7. }
  8. //关闭cursor
  9. cursor.close();
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. }
  • cursor.close(); 游标一定要关闭,不然连接会一直增长;可以使用client lists``info clients``info stats命令查看客户端连接状态,会发现scan操作一直存在
  • 我们平时使用的redisTemplate.execute 是会主动释放连接的,可以查看源码确认
  1. client list
  2. ......
  3. 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
  4. ......
  5. org.springframework.data.redis.core.RedisTemplate#execute(org.springframework.data.redis.core.RedisCallback<T>, boolean, boolean)
  6. finally {
  7. RedisConnectionUtils.releaseConnection(conn, factory);
  8. }

2. scan

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

  • 这个 connection.scan 没法移动cursor,也只能scan一次
  1. public Set<String> scan(String matchKey) {
  2. Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
  3. Set<String> keysTmp = new HashSet<>();
  4. Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match("*" + matchKey + "*").count(1000).build());
  5. while (cursor.hasNext()) {
  6. keysTmp.add(new String(cursor.next()));
  7. }
  8. return keysTmp;
  9. });
  10. return keys;
  11. }

2.2 使用 MultiKeyCommands

  • 获取 connection.getNativeConnectionconnection.getNativeConnection()实际对象是Jedis(debug可以看出) ,Jedis实现了很多接口
  1. public class Jedis extends BinaryJedis implements JedisCommands, MultiKeyCommands, AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands
  • 当 scan.getStringCursor() 存在 且不是 0 的时候,一直移动游标获取
  1. public Set<String> scan(String key) {
  2. return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
  3. Set<String> keys = Sets.newHashSet();
  4. JedisCommands commands = (JedisCommands) connection.getNativeConnection();
  5. MultiKeyCommands multiKeyCommands = (MultiKeyCommands) commands;
  6. ScanParams scanParams = new ScanParams();
  7. scanParams.match("*" + key + "*");
  8. scanParams.count(1000);
  9. ScanResult<String> scan = multiKeyCommands.scan("0", scanParams);
  10. while (null != scan.getStringCursor()) {
  11. keys.addAll(scan.getResult());
  12. if (!StringUtils.equals("0", scan.getStringCursor())) {
  13. scan = multiKeyCommands.scan(scan.getStringCursor(), scanParams);
  14. continue;
  15. } else {
  16. break;
  17. }
  18. }
  19. return keys;
  20. });
  21. }

发散思考

cursor没有close,到底谁阻塞了,是 Redis 么

  • 测试过程中,我基本只要发起十来个scan操作,没有关闭cursor,接下来的请求都卡住了

redis侧分析

  • client lists``info clients``info stats查看

    发现 连接数 只有 十几个,也没有阻塞和被拒绝的连接
  • config get maxclients查询redis允许的最大连接数 是 10000
  1. 1) "maxclients"
  2. 2) "10000"`
  • redis-cli在其他机器上也可以直接登录 操作

综上,redis本身没有卡死

应用侧分析

  • netstat查看和redis的连接,6333是redis端口;连接一直存在
  1. ~ netstat -an | grep 6333
  2. netstat -an | grep 6333
  3. tcp4 0 0 xx.xx.xx.aa.52981 xx.xx.xx.bb.6333 ESTABLISHED
  4. tcp4 0 0 xx.xx.xx.aa.52979 xx.xx.xx.bb.6333 ESTABLISHED
  5. tcp4 0 0 xx.xx.xx.aa.52976 xx.xx.xx.bb.6333 ESTABLISHED
  6. tcp4 0 0 xx.xx.xx.aa.52971 xx.xx.xx.bb.6333 ESTABLISHED
  7. tcp4 0 0 xx.xx.xx.aa.52969 xx.xx.xx.bb.6333 ESTABLISHED
  8. tcp4 0 0 xx.xx.xx.aa.52967 xx.xx.xx.bb.6333 ESTABLISHED
  9. tcp4 0 0 xx.xx.xx.aa.52964 xx.xx.xx.bb.6333 ESTABLISHED
  10. tcp4 0 0 xx.xx.xx.aa.52961 xx.xx.xx.bb.6333 ESTABLISHED
  • jstack查看应用的堆栈信息

    发现很多 WAITING 的 线程,全都是在获取redis连接

    所以基本可以断定是应用的redis线程池满了
  1. "http-nio-7007-exec-2" #139 daemon prio=5 os_prio=31 tid=0x00007fda36c1c000 nid=0xdd03 waiting on condition [0x00007000171ff000]
  2. java.lang.Thread.State: WAITING (parking)
  3. at sun.misc.Unsafe.park(Native Method)
  4. - parking to wait for <0x00000006c26ef560> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
  5. at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
  6. at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
  7. at org.apache.commons.pool2.impl.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:590)
  8. at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:441)
  9. at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:362)
  10. at redis.clients.util.Pool.getResource(Pool.java:49)
  11. at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226)
  12. at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16)
  13. at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:276)
  14. at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:469)
  15. at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:132)
  16. at org.springframework.data.redis.core.RedisTemplate.executeWithStickyConnection(RedisTemplate.java:371)
  17. 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. 1) "timeout"
  2. 2) "0"
  • netstat查看和redis的连接,6333是redis端口;连接从ESTABLISHED变成了CLOSE_WAIT;
  • jstack和 原来表现一样,卡在JedisConnectionFactory.getConnection
  1. ~ netstat -an | grep 6333
  2. netstat -an | grep 6333
  3. tcp4 0 0 xx.xx.xx.aa.52981 xx.xx.xx.bb.6333 CLOSE_WAIT
  4. tcp4 0 0 xx.xx.xx.aa.52979 xx.xx.xx.bb.6333 CLOSE_WAIT
  5. tcp4 0 0 xx.xx.xx.aa.52976 xx.xx.xx.bb.6333 CLOSE_WAIT
  6. tcp4 0 0 xx.xx.xx.aa.52971 xx.xx.xx.bb.6333 CLOSE_WAIT
  7. tcp4 0 0 xx.xx.xx.aa.52969 xx.xx.xx.bb.6333 CLOSE_WAIT
  8. tcp4 0 0 xx.xx.xx.aa.52967 xx.xx.xx.bb.6333 CLOSE_WAIT
  9. tcp4 0 0 xx.xx.xx.aa.52964 xx.xx.xx.bb.6333 CLOSE_WAIT
  10. 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
  1. redis.clients.jedis.JedisPoolConfig
  2. private int maxTotal = 8;
  3. private int maxIdle = 8;
  4. private int minIdle = 0;
  • 如何配置更大的连接池呢?

    A. 原配置
  1. @Bean
  2. public RedisConnectionFactory redisConnectionFactory() {
  3. RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
  4. redisStandaloneConfiguration.setHostName(redisHost);
  5. redisStandaloneConfiguration.setPort(redisPort);
  6. redisStandaloneConfiguration.setPassword(RedisPassword.of(redisPasswd));
  7. JedisConnectionFactory cf = new JedisConnectionFactory(redisStandaloneConfiguration);
  8. cf.afterPropertiesSet();
  9. return cf;
  10. }

readTimeout,connectTimeout不指定,有默认值 2000 ms

  1. org.springframework.data.redis.connection.jedis.JedisConnectionFactory.MutableJedisClientConfiguration
  2. private Duration readTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
  3. private Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);

B. 修改后配置

    1. 配置方式一:部分接口已经Deprecated了
  1. @Bean
  2. public RedisConnectionFactory redisConnectionFactory() {
  3. JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
  4. jedisPoolConfig.setMaxTotal(16); // --最多可以建立16个连接了
  5. jedisPoolConfig.setMaxWaitMillis(10000); // --10s获取不到连接池的连接,
  6. // --直接报错Could not get a resource from the pool
  7. jedisPoolConfig.setMaxIdle(16);
  8. jedisPoolConfig.setMinIdle(0);
  9. JedisConnectionFactory cf = new JedisConnectionFactory(jedisPoolConfig);
  10. cf.setHostName(redisHost); // -- @Deprecated
  11. cf.setPort(redisPort); // -- @Deprecated
  12. cf.setPassword(redisPasswd); // -- @Deprecated
  13. cf.setTimeout(30000); // -- @Deprecated 貌似没生效,30s超时,没有关闭连接池的连接;
  14. // --redis没有设置超时,会一直ESTABLISHED;redis设置了超时,且超时之后,会一直CLOSE_WAIT
  15. cf.afterPropertiesSet();
  16. return cf;
  17. }
    1. 配置方式二:这是群里好友给找的新的配置方式,效果一样
  1. RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
  2. redisStandaloneConfiguration.setHostName(redisHost);
  3. redisStandaloneConfiguration.setPort(redisPort);
  4. redisStandaloneConfiguration.setPassword(RedisPassword.of(redisPasswd));
  5. JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
  6. jedisPoolConfig.setMaxTotal(16);
  7. jedisPoolConfig.setMaxWaitMillis(10000);
  8. jedisPoolConfig.setMaxIdle(16);
  9. jedisPoolConfig.setMinIdle(0);
  10. cf = new JedisConnectionFactory(redisStandaloneConfiguration, JedisClientConfiguration.builder()
  11. .readTimeout(Duration.ofSeconds(30))
  12. .connectTimeout(Duration.ofSeconds(30))
  13. .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

在RedisTemplate中使用scan代替keys指令的更多相关文章

  1. redis中使用SCAN代替KEYS

    前言 由于redis的keys命令是线上禁用,所以就有了SCAN.SSCAN.HSCAN和ZSCAN四个命令. 但是这四个命令也不是每次返回全部匹配结果,因此需要一遍遍执行下去,而且每次返回的curs ...

  2. redis中scan和keys的区别

    scan和keys的区别 redis的keys命令,通来在用来删除相关的key时使用,但这个命令有一个弊端,在redis拥有数百万及以上的keys的时候,会执行的比较慢,更为致命的是,这个命令会阻塞r ...

  3. Oracle 11G R2 RAC中的scan ip 的用途和基本原理【转】

    Oracle 11G R2 RAC增加了scan ip功能,在11.2之前,client链接数据库的时候要用vip,假如你的cluster有4个节点,那么客户端的tnsnames.ora中就对应有四个 ...

  4. freemarker中8个常用的指令

    这里列举出Freemarker模板文件中8个常用的指令. 1. assign assign指令用于创建或替换一个顶层变量,assign指令的用法有多种,包括创建或替换一个顶层变量,创建或替换多个变量等 ...

  5. 【Azure Redis 缓存】Azure Cache for Redis 中如何快速查看慢指令情况(Slowlogs)

    问题描述 当 Azure Redis 服务器负载过高的情况下,使用时就会遇见连接超时,命令超时,IO Socket超时等异常.为了能定位是那些因素引起的,可以参考微软官方文档( 管理 Azure Ca ...

  6. Redis中的Scan命令的使用

    Redis中有一个经典的问题,在巨大的数据量的情况下,做类似于查找符合某种规则的Key的信息,这里就有两种方式,一是keys命令,简单粗暴,由于Redis单线程这一特性,keys命令是以阻塞的方式执行 ...

  7. RedisTemplate 中 opsForHash()使用 (没有测试过,copy的)

    1.put(H key, HK hashKey, HV value) //新增hashMap值 redisTemplate.opsForHash().put("hashValue" ...

  8. redis 用scan 代替keys,hgetAll

    转载自:https://blog.csdn.net/w05980598/article/details/80264568 众所周知,当redis中key数量越大,keys 命令执行越慢,而且最重要的会 ...

  9. Redis中的Scan命令踩坑记

    1 原本以为自己对redis命令还蛮熟悉的,各种数据模型各种基于redis的骚操作.但是最近在使用redis的scan的命令式却踩了一个坑,顿时发觉自己原来对redis的游标理解的很有限.所以记录下这 ...

随机推荐

  1. ANT 的使用

    概述 ant 是一个将软件编译.测试.部署等步骤联系在一起加以自动化的一个工具,大多用于 Java 环境中的软件开发. 在与 Jmeter 生成的 jmx 文件配合使用中,ant 会完成jmx计划的执 ...

  2. Linux从入门到放弃、零基础入门Linux(第三篇):在虚拟机vmware中安装linux(二)超详细手把手教你安装centos6分步图解

    一.继续在vmware中安装centos6.9 本次安装是进行最小化安装,即没有图形化界面的安装,如果是新手,建议安装带图形化界面的centos, 具体参考Linux从入门到放弃.零基础入门Linux ...

  3. MySQL数据库(一)-- 数据库介绍、MySQL安装、基础SQL语句

    一.数据库介绍 1.什么是数据库 数据库即存储数据的仓库 2.为什么要用数据库 (1)用文件存储是和硬盘打交道,是IO操作,所以有效率问题 (2)管理不方便 (3)一个程序不太可能仅运行在同一台电脑上 ...

  4. 彻底理解webgl

    javascript很简单,核心点就一个: 一切皆对象. 简单又熟悉.呵呵 这么简单的一句话,理解后,你就掌握了js. 一切皆对象,函数也是对象,创建静态方法 fun.action, 创建实例:new ...

  5. 性能测试基础---jmeter二次开发

    ·Jmeter的二次开发,常见的有以下几种类型: ·扩展.修改Jmeter已有的组件(源代码) ·扩展.修改Jmeter已有的函数. ·完全自主开发一个新的组件(依赖于Jmeter提供的框架). ·扩 ...

  6. S3C2440_LCD控制器

    1.LCD控制器主要有两方面的功能: 1)从framebuffer中取出某个像素的数据: 2)配合其他信号,一起将这个数据发送给LCD 不管是2440,还是其他型号的ARM芯片.它们的LCD控制器的功 ...

  7. 云服务器ECS(Elastic Compute Service),知识点

    资料 网址 什么是云服务器ECS https://help.aliyun.com/document_detail/25367.html?spm=a2c4g.11186623.6.544.4e1e376 ...

  8. 01-赵志勇机器学习-Logistics_Regression-train

    Logistics Regression 二分类问题. 模型 线性模型 响应 sigmoid 损失函数(显示) 最小均方 优化方法 BGD 例子: #coding utf-8 import numpy ...

  9. volatile 关键字 和 i++ 原子性

    package com.mozq.multithread; /** * 深入理解Java虚拟机 volatile 关键字 和 i++ 原子性. */ public class VolatileTest ...

  10. pointnet++论文的翻译

    参考: https://blog.csdn.net/weixin_40664094/article/details/83902950 https://blog.csdn.net/pikachu_777 ...