理解redis SCAN

应用场景

一个使用场景是要找到特定格式的key做处理,删除或更新。

虽然另一个命令KEYS可以实现这个功能,但是跟据官方文档中的说明,如果数据库中存在大量的key那么这个命令会阻塞服务数秒甚至更久的时间,对于大多数业务来说都是不可接受的。

如果用SQL表示KEYS命令类似下面语句,可以预料如果库中数据量很大,执行这个代码将是魔鬼操作

SELECT key FROM keys;

于是redis在2.8版本之后新增了SCAN命令,原理其实就是把KEYS拆解成一个个小段来执行。用SQL表示就类似下面的操作,显然这样操作不管数据量多大,只要控制好LIMIT都不会太差。

SELECT id, key FROM keys WHERE id>0 LIMIT 10;
100   key1
...
109   key10
SELECT id, key FROM keys WHERE id>109 LIMIT 10;
110   key11
...

更多参数

文档中SCAN命令的完整格式是下面这个样子的。

SCAN cursor [MATCH pattern] [COUNT count]

  • cursor(游标)就是上面的id>xxx的那个数字,每次传入上次扫描的结束位置
  • pattern是匹配模式,比如我要筛出user前缀的key就传入user*
  • count类似上面SQL中的LIMIT

但是模式匹配有个陷阱,我们执行 SCAN 0 MATCH user* COUNT 10 之后可能发现并没有返回任何东西。

这个时候你不要怀疑这个命令bug了或者redis版本有问题(只要大于2.8),因为pattern并不影响扫描过程,用SQL来解释就是无论是否指定pattern都是执行上面那个扫描语句,而不是变成

SELECT id, key FROM keys WHERE id>109 AND key LIKE 'pattern' LIMIT 10;

这下懂了吧,pattern只是对SCAN的结果做筛选,如果SCAN了10条都没有这个模式出现那就返回空咯。但是你的SCAN还要按原计划继续执行下去。

应用

网上有一种删除key的命令,可以给大家参考,但是我没有执行成功,会报–scan参数错误,不知什么原因。

redis-cli --scan --pattern "ops-coffee-*" | xargs -L 2000 redis-cli del 

我用PHP实现了一个简单的脚本来做这件事

    function deleteByPattern($pattern, $batchCount = 200){
        if(empty($pattern)){
            return;
        }

        $it = NULL;
        do {
            $arr_keys = $redis->scan($it, $pattern, $batchCount); 

            if ($arr_keys !== FALSE) {
                foreach($arr_keys as $str_key) {
                    $redis->del($str_key);
                    echo "deleted key: $str_key\n";
                }
            }
        } while ($it > 0);
    }

参考资料

http://doc.redisfans.com/key/scan.html

https://juejin.im/post/5d06eba4e51d45775e33f55e

https://github.com/phpredis/phpredis#scan

发表评论