Redis 中 Keys 与 Scan 的使用

@这是小豪的第十二篇文章

上篇文章 发布后,在公众号下方的留言中看到了这么一句话 "keys 操作有点恐怖哈",当时就在想为啥恐怖,有点不知所以然,也没过多的去管他,直到今天在社区文章中又看到了人家的提醒 "keys 操作是不行的".... 哈哈,需要去了解为什么啦!

keys 是什么

  • keys - KEYS PATTERN

    用于查找所有符合给定模式 pattern 的 key

为什么会用到 keys

$redis->keys('login:201903*')
$redis->bitop('AND',  'monthActivities'', $redis->keys('login:201903*'));

echo "连续一个月签到用户数量:" . $redis->bitCount('monthActivities');

这是上篇文章中用到 keys 的地方,当时是为了统计连续一个月签到用户数量,通过 keys('login:201903') 获取到三月所有 key ,然后加以聚合统计。

keys 使用会造成的后果

大家知道 Redis 是单线程程序,是按照顺序执行指令的,如果说我们现在正在执行 keys 命令,那么其它指令必须等到当前的 keys 指令执行完了才可以继续,再加上 keys 操作是遍历算法,复杂度是 O(n),乍一想就知道问题所在了,当实例中数据量过大的时候,Redis 服务可能会卡顿,其余指令可能会延时甚至超时报错....

再者 keys 中没有 offset、limit 参数,如果说满足查询条件的 keys 特别多,那就有点尴尬了,哈哈。

所以说官方的建议是:生产环境屏蔽掉 keys 命令。

替代方案 scan

说了那么多,这也不行那也不好的,究竟怎么办呢,Redis 为了解决这个问题,它在 2.8 版本中加入了指令:scan。

好,那我们现在就来看一下这个命令:

  • scan - cursor [MATCH pattern] [COUNT count]

    用于迭代当前数据库中的数据库键

相比 keys ,我们来看一下 scan 的特点:

  • 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;
  • 提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是对增量式迭代命令的一种提示 (hint),返回的结果可多可少;
  • 同 keys 一样,它也提供模式匹配功能;
  • 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
  • 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  • 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  • 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零

现在我们来实践一下:

> keys 201903*
    1) "login:20190311"
    2) "login:20190312"
    3) "login:20190313"
> scan 0 match login:201903*
    1) "0"
    2) 1) "login:20190313"
       2) "login:20190311"
       3) "login:20190312"
> scan 0 match login:201903* count 2
    1) "5"
    2) 1) "login:20190313"
       2) "login:20190311"

看到这里估计有点蒙圈,scan 0 是个啥意思,为啥下面的结果中第一个数据有的为 0 ,有的为 5。

其实是这样的,当我们第一次遍历查询时,cursor 值为 0,如果说数据全部查询完毕,那么返回结果的第一个数据就为 0 表示查询完毕,如果说返回的不为 0 ,那么就需要将这个数据作为下一次遍历的 cursor,也就是现在的 scan 5,一直遍历到返回的第一个数据为 0 为止。第二个参数一目了然哈,就是控制返回数量,那究竟是不是这样呢,我们来看一下:

> scan 0
    1) "0"
    2) 1) "age"
       2) "login:20190313"
       3) "names"
       4) "login:20190311"
       5) "name"
       6) "login:20190312"
       7) "sex"
       8) "ages"
> scan 0 match login:201903* count 2
    1) "1"
    2) 1) "login:20190313"

现在问题就来了,不是 count 2 吗。怎么查询出来的数据只有 1 条,是不是坏了?不是滴,哈哈。这是因为这个 count 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于),所以就明白了吧。它查出来是空的也不要担心,只要结果的第一个数据不为 0 就继续遍历循环下去。

说到这里,大家应该对,scan 有了大致的了解了吧,那我们就回归到之前的问题,统计该如何修改呢,大家来看一下基本的查询代码:

\dd($redis->scan(0, 'match', 'login:201903*', 'count', 1000));

来看一下结果:
file

就这样就 ok 啦,至于统计代码该如何改,我就没写出来哒,大家可以自己思考一下怎么去写,然后贴在评论区,哈哈。

结束语

至此 keysscan 的讲解就结束哒,有什么不明白的或者有错误的地方,还望大家在评论区留言。

相关链接:

本作品采用《CC 协议》,转载必须注明作者和本文链接
finecho # Lhao
本帖由系统于 4年前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 12

666,高产似母猪! :+1:

5年前 评论

老母猪 🐷

5年前 评论

3.2.12版本,为什么scan 0 match login:201903*第一个返回值不是0

127.0.0.1:6379> info
# Server
redis_version:3.2.12
redis_git_sha1:00000000
redis_git_dirty:0
127.0.0.1:6379> setbit login:20190321 1 1
(integer) 0
127.0.0.1:6379> setbit login:20190321 2 1
(integer) 0
127.0.0.1:6379> setbit login:20190322 2 1
(integer) 0
127.0.0.1:6379> setbit login:20190323 2 1
(integer) 0
127.0.0.1:6379> keys login:201903*
1) "login:20190323"
2) "login:20190322"
3) "login:20190321"
127.0.0.1:6379> scan 0 match login:201903*
1) "3"
2) 1) "login:20190323"
   2) "login:20190322"

127.0.0.1:6379> scan 0 match login:201903* count 2
1) "4"
2) (empty list or set)
127.0.0.1:6379> scan 0 match login:201903* count 20
1) "0"
2) 1) "login:20190323"
   2) "login:20190322"
   3) "login:20190321"
5年前 评论
finecho

@lovecn COUNT 参数的默认值为 10 噢,如果不是 0 ,你再使用 scan 3 match login:201903* 继续遍历。

5年前 评论

高产的母猪 :+1:

4年前 评论

遇到一个问题,使用facades的Redis 是不是没办法使用scan?我尝试使用返回的偏移一直不变 你有使用过么?

4年前 评论
shengxiao 4年前
sleet (作者) 4年前
LEIQINGHE 3年前
finecho

@sleet 你有 connection 吗

4年前 评论
sleet 4年前

谢谢分享,高产母猪

4年前 评论

那这是啥情况,count是遍历的整个库里面key?还是遍历的ceshi这个key里面的值

127.0.0.1:6379> ZSCAN ceshi 0 match *1* count 2
1) "0"
2)  1) "1"
    2) "1"
    3) "10000"
    4) "10000"
    5) "10001"
    6) "10001"
    7) "10002"
    8) "10002"
    9) "10003"
   10) "10003"
   11) "10004"
   12) "10004"
   13) "10005"
   14) "10005"
   15) "100000"
   16) "100000"
4年前 评论
//使用scan匹配all key
function scanAllForMatch($pattern, $cursor=null, $results=[]) {

    if ($cursor === "0") {
        return $results;
    }

    if ($cursor === null) {
        $cursor = "0";
    }

    $redis = Cache::getRedis();
    list($cursor, $result) = $redis->scan($cursor, 'match', $pattern, 'count', 100);

    $results = array_merge($results, $result);

    return scanAllForMatch($pattern, $cursor, $results);
}
3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!