redis的扫描方法,使用scan,而不是使用 keys*
因为keys* 会全部key扫描一次,key数量很多时,容易造成阻塞太久甚至down机。
scan 原理: 指定每次遍历的key数目和查找规则 ,通过遍历去匹配出对应的key。还会返回当前最后一个匹配值的游标cursor
scan 语法: SCAN cursor [MATCH pattern] [COUNT count]
cursor - 游标。从0开始
pattern - 匹配的规则。
count - 指定每次遍历多少个key。
可以简单理解为每次遍历多少个元素
根据测试,推荐 Count大小为 10000
因此:Count 参数和 Key的总数 一致时,Scan 命令就和 Keys 效果一样了。Count 参数越大,总时间越短,但每次Redis 阻塞时间也会越长,需要取舍。
scan用法:
SCAN命令是基于游标的,每次调用后,都会返回一个游标,用于下一次迭代。当游标返回0时,表示迭代结束。
第一次 Scan 时指定游标为 0,表示开启新的一轮迭代,然后 Scan 命令返回一个新的游标,作为第二次 Scan 时的游标值继续迭代,一直到 Scan 返回游标为0,表示本轮迭代结束。
eg: 我想查找 test 开头的key,每次查找30条
1.第一次执行找到了test_b,返回了下个游标 17 。
scan 0 match test* count 30
2.使用上次返回的游标,再次查找,又找到了 test_a 和 test
scan 17 match test* count 30
此时游标返回0,表示结束,一共找到 test_a test_b test 三个key
在PHP的用法:
/**
* 查找redis key
* @param null $pattern // 要匹配的规则 'test_*'
* @param int $count // 每次遍历数量.count越大总耗时越短,但单次阻塞越长。 建议5000-10000。并发不高则可以调至接近1w。
* @return array
*/
public function scan($pattern,$count = 6000){
$keyArr = array();
while (true){
// $iterator 下条数据的坐标
$data = $this->redis->scan($iterator, $pattern, $count);
$keyArr = array_merge($keyArr,$data ?: array() );
if ($iterator === 0){ //迭代结束,未找到匹配
break;
}
if ($iterator === null) {//"游标为null了,重置为0,继续扫描"
$iterator = "0";
}
}
$keyArr = array_flip($keyArr);
$keyArr = array_flip($keyArr);
return $keyArr;
}
//使用 查找test开头的key
$pattern = 'test*';
$this->scan($pattern);
查出来key之后,若要批量删除,则可以使用redis管道 PIPELINE ,效果是 将多个命令合起来只执行一次,减少redis和客户端的交互时间;
其他批量操作也可以用PIPELINE,下面举个删除的例子:
$keyArr = array('test','test_a','test_b');
$pipe = $redis->multi(2); //使用管道 事务=2,表示使用管道
foreach ($keyArr as $key){
$pipe->del($key);
}
$pipe->exec();
最后来个 PIPELINE结合scan的用法:
// 返回查询的redis key
function redisScan($pattern = null,$count = 6000,$is_del = 1){
$redis = RedisClient::getInstance();
$keyArr = $redis->scan($pattern,$count); // 上面的scan方法
if ($is_del){
$pipe = $redis->multi(2); //使用管道
foreach ($keyArr as $key){
$pipe->del($key);
}
$pipe->exec();
}else{
return $keyArr ?? [];
}
}
//清除缓存
$pattern = 'test*';
$this->redisScan($pattern);
注意:redis管道类似事务,multi和exec之间不能包含mysql操作
有话要说