php redis之高性能扫描和批量操作

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操作


有话要说