Redis遍历所有key的两个命令 -- KEYS 和 SCAN

结论: 

Redis 中的 SCAN 命令和 KEYS 命令都可以用来搜索符合条件的键,但它们的实现方式不同。SCAN 命令使用游标式迭代方式,而 KEYS 命令则需要读取所有的键,因此会导致堵塞服务。

因为它需要在 Redis 数据库中遍历所有键,并返回与指定模式匹配的结果。具体来说,当执行 KEYS 命令时,Redis 会暂停处理其他客户端的请求,直到该操作完成。

这种阻塞行为主要是由于 Redis 采用了单线程模型的原因。在 Redis 中,所有的读写操作都是在同一个线程中进行的。当执行 KEYS 命令时,Redis 会将当前线程中的 CPU 时间全部用于执行该操作,这就导致了其他客户端的请求被阻塞。

此外,由于 Redis 是内存数据库,当数据量较大时,执行 KEYS 命令可能会耗费大量的 CPU 和内存资源。这也意味着随着 Redis 数据库中数据量的增加,KEYS 命令的执行时间和资源消耗会越来越高,从而对系统的性能造成负面影响。

 

具体来说,KEYS 命令会在 Redis 服务器中遍历所有键,并返回与指定模式匹配的结果。如果 Redis 数据库中包含大量的键,这个操作可能会非常耗时,并对 Redis 服务器的性能产生负面影响。此外,由于 KEYS 命令是阻塞命令,所以在执行期间,其他客户端的请求也会被阻塞。

相比之下,SCAN 命令使用游标的方式来遍历数据集中的元素,每次只返回一小部分数据,并不会一次性读取整个数据集,因此不会像 KEYS 命令那样耗费大量的时间和资源。此外,由于 SCAN 命令是非阻塞命令,所以不会对其他客户端的请求造成影响。

综上所述,为了避免 Redis 服务器被 KEYS 命令阻塞,应该尽量使用 SCAN 命令来获取数据。当然,为了进一步提高 Redis 的性能,还可以采用以下策略:

  • 对于大型数据集,可以将其分割成多个小的数据集,并在需要时只查询其中的一部分。
  • 对于需要频繁进行搜索的数据集,可以使用 Redis 的有序集合(Sorted Set)来代替。
  • 在客户端和 Redis 服务器之间增加缓存层,以减少对 Redis 数据库的访问次数。
  • 在 Redis 服务器上启用持久化功能,以便在服务器出现故障时可以快速恢复数据。


遍历Redis所有key或者指定模式的key:  KEYS pattern

官网对于KEYS命令有一个提示: KEYS 的速度非常快,例如,Redis在一个有1百万个key的数据库里面执行一次查询需要的时间是40毫秒 。但在一个大的数据库中使用它仍然可能造成性能问题,如果你需要从一个数据集中查找特定的 KEYS, 你最好还是用 Redis 的集合结构 SETS 来代替。由于KEYS命令一次性返回所有匹配的key,所以,当redis中的key非常多时,对于内存的消耗和redis服务器都是一个隐患,

Redis 2.8以上版本提供了一个更好的遍历key的命令 SCAN,  该命令的基本格式:

SCAN cursor [MATCH pattern] [COUNT count]

SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):

  • SCAN 命令用于迭代当前数据库中的数据库键。
  • SSCAN 命令用于迭代集合键中的元素。
  • HSCAN 命令用于迭代哈希键中的键值对。
  • ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

以上列出的四个命令都支持增量式迭代, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS 命令、 SMEMBERS 命令带来的问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。

SCAN 命令, 以及其他增量式迭代命令, 在进行完整遍历的情况下可以为用户带来以下保证: 从完整遍历开始直到完整遍历结束期间, 一直存在于数据集内的所有元素都会被完整遍历返回; 这意味着如果有一个元素, 它从遍历开始直到遍历结束期间都存在于被遍历的数据集当中, 那么 SCAN 命令总会在某次迭代中将这个元素返回给用户。

然而因为增量式命令仅仅使用游标来记录迭代状态, 所以这些命令带有以下缺点:

  • 同一个元素可能会被返回多次。 处理重复元素的工作交由应用程序负责, 比如说, 可以考虑将迭代返回的元素仅仅用于可以安全地重复执行多次的操作上。
  • 如果一个元素是在迭代过程中被添加到数据集的, 又或者是在迭代过程中从数据集中被删除的, 那么这个元素可能会被返回, 也可能不会, 这是未定义的(undefined)。

因为 SCAN 、 SSCAN 、 HSCAN 和 ZSCAN 四个命令的工作方式都非常相似, 所以这个文档会一并介绍这四个命令, 但是要记住:

  • SSCAN 命令、 HSCAN 命令和 ZSCAN 命令的第一个参数总是一个数据库键。
  • 而 SCAN 命令则不需要在第一个参数提供任何数据库键 —— 因为它迭代的是当前数据库中的所有数据库

SCAN命令是一个基于游标的迭代器。这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程

当SCAN命令的游标参数(即cursor)被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束

简单的迭代演示:

redis 127.0.0.1:6379> scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
   10) "key:7"
   11) "key:1"
redis 127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"


在上面这个例子中, 第一次迭代使用 0 作为游标, 表示开始一次新的迭代。第二次迭代使用的是第一次迭代时返回的游标 17 ,作为新的迭代参数 。

显而易见,SCAN命令的返回值 是一个包含两个元素的数组, 第一个数组元素是用于进行下一次迭代的新游标, 而第二个数组元素则又是一个数组, 这个数组中包含了所有被迭代的元素。

注意:返回的游标不一定是递增的,可能后一次返回的游标比前一次的小。

在第二次调用 SCAN 命令时, 命令返回了游标 0 , 这表示迭代已经结束, 整个数据集已经被完整遍历过了

full iteration :以 0 作为游标开始一次新的迭代, 一直调用 SCAN 命令, 直到命令返回游标 0 , 我们称这个过程为一次完整遍历。

SCAN增量式迭代命令并不保证每次执行都返回某个给定数量的元素,甚至可能会返回零个元素, 但只要命令返回的游标不是 0 , 应用程序就不应该将迭代视作结束。

不过命令返回的元素数量总是符合一定规则的, 对于一个大数据集来说, 增量式迭代命令每次最多可能会返回数十个元素;而对于一个足够小的数据集来说,可能会一次迭代返回所有的key

COUNT选项
对于增量式迭代命令不保证每次迭代所返回的元素数量,我们可以使用COUNT选项, 对命令的行为进行一定程度上的调整。COUNT 选项的作用就是让用户告知迭代命令, 在每次迭代中应该从数据集里返回多少元素, COUNT 参数的默认值为 10 。
使用COUNT 选项对于对增量式迭代命令相当于一种提示, 大多数情况下这种提示都比较有效的控制了返回值的数量。

注意:COUNT选项并不能严格控制返回的key数量,只能说是一个大致的约束。并非每次迭代都要使用相同的 COUNT 值,用户可以在每次迭代中按自己的需要随意改变 COUNT 值, 只要记得将上次迭代返回的游标用到下次迭代里面就可以了。

MATCH 选项
类似于KEYS 命令,增量式迭代命令通过给定 MATCH 参数的方式实现了通过提供一个 glob 风格的模式参数, 让命令只返回和给定模式相匹配的元素。

MATCH 选项对元素的模式匹配工作是在命令从数据集中取出元素后和向客户端返回元素前的这段时间内进行的, 所以如果被迭代的数据集中只有少量元素和模式相匹配, 那么迭代命令或许会在多次执行中都不返回任何元素。

以下是这种情况的一个例子:

 redis 127.0.0.1:6379> scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
redis 127.0.0.1:6379> scan 288 MATCH *11*
1) "224"
2) (empty list or set)
redis 127.0.0.1:6379> scan 224 MATCH *11*
1) "80"
2) (empty list or set)
redis 127.0.0.1:6379> scan 80 MATCH *11*
1) "176"
2) (empty list or set)
redis 127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000
1) "0"
2)  1) "key:611"
    2) "key:711"
    3) "key:118"
    4) "key:117"
    5) "key:311"
    6) "key:112"
    7) "key:111"
    8) "key:110"
    9) "key:113"
   10) "key:211"
   11) "key:411"
   12) "key:115"
   13) "key:116"
   14) "key:114"
   15) "key:119"
   16) "key:811"
   17) "key:511"
   18) "key:11"
redis 127.0.0.1:6379>
可以看出,以上的大部分迭代都不返回任何元素。在最后一次迭代, 我们通过将 COUNT 选项的参数设置为 1000 , 强制命令为本次迭代扫描更多元素, 从而使得命令返回的元素也变多了。

java代码演示: 

SpringRedisTemplate针对这个Scan进行了封装,示例使用(针对最新库spring-data-redis-1.8.1.RELEASE):

Set<Object> execute = redisTemplate.execute(new RedisCallback<Set<Object>>() {

    @Override
    public Set<Object> doInRedis(RedisConnection connection) throws DataAccessException {

        Set<Object> binaryKeys = new HashSet<>();

        Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match("test*").count(1000).build());
        while (cursor.hasNext()) {
            binaryKeys.add(new String(cursor.next()));
        }
        return binaryKeys;
    }
});

注意Cursor一定不能关闭,在之前的版本中,这里Cursor需要手动关闭,但是从1.8.0开始,不能手动关闭!否则会报异常。

ScanOptions有两个参数,一个是match,另一个是count,分别对应scan命令的两个参数。

可以看出,Redis的SCAN操作由于其整体的数据设计,无法提供特别准的scan操作,仅仅是一个“can ‘ t guarantee , just do my best”的实现:

提供键空间的遍历操作,支持游标,复杂度O(1), 整体遍历一遍只需要O(N);
提供结果模式匹配;
支持一次返回的数据条数设置,但仅仅是个hints,有时候返回的会多;
弱状态,所有状态只需要客户端需要维护一个游标;
无法提供完整的快照遍历,也就是中间如果有数据修改,可能有些涉及改动的数据遍历不到;
每次返回的数据条数不一定,极度依赖内部实现;
返回的数据可能有重复,应用层必须能够处理重入逻辑;上面的示例代码中,redisTemplate.execute方法是个Set,相当于已经对于返回的key去重

//原生redis scan+keys命令查询
public static void main(String[] args) {
        Jedis jedis = new Jedis("101.132.164.224",6379);
        jedis.auth("opec123456");
        jedis.select(1);

        jedis.set("freigh1","1");
        jedis.set("freigh2","2");
        jedis.set("freigh3","3");

        //方法1:  keys命令查找所有的key
        //Set<String> s = jedis.keys("freigh*");

        //方法2:  scan命令查找所有的key
        Set<String> s2 = new HashSet<>();
        String cursor = ScanParams.SCAN_POINTER_START;
        ScanParams scanParams = new ScanParams();
        scanParams.count(1);
        scanParams.match("freigh*");
        do {
            ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
            s2.addAll(scanResult.getResult());
            //将返回的参数 作为下一次的游标
            cursor = scanResult.getStringCursor();
        }while(!"0".equals(cursor));


        Long del = jedis.del(s2.toArray(new String[s2.size()]));
        System.err.println(del);
}
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值