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 的讲解就结束哒,有什么不明白的或者有错误的地方,还望大家在评论区留言。

相关链接:

finecho # Lhao

本帖由系统于 1个月前 自动加精
finecho
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 9

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

3个月前 评论

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"
3个月前 评论
finecho

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

3个月前 评论

高产的母猪 :+1:

2个月前 评论

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

1周前 评论
shengxiao: 我也遇到了这个问题,原因是 facades 通过 _ call 来调用 scan 方法,而 _call 等魔术方法是没法引用传参的,导致 scan 的第一个参数(迭代游标)无法正确设置。解决方案是通过 client() 方法获取到 Redis 对象再调用 scan 方法。 3小时前
finecho

@sleet 你有 connection 吗

1周前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!