Redis 实用小技巧——批量删除指定的 key

日常工作当中经常会遇到删除 Redis key 的问题,如果是删除某个 key ,使用 DEL {keyname} 或者 EXPIRE {keyname} {ttl} 都可以实现。但如果想要一次性删除多个 key 应该怎么处理呢?你可能会想到直接使用 DEL key [key ...] 的方式来处理,但是当要删除的 key 有很多呢?或者我们事先并不能确认要删除的是哪些 key 呢?(比如通过搜索条件匹配出来的 key )

下面我们就来看看如何巧妙地处理这类问题。

场景一:删除所有的 key

如果需要执行初始化的操作,清理掉数据库所有的键,可以使用 FLUSHDB 或者 FLUSHALL 命令操作。

FLUSHDB:删除当前数据库中的所有 key 。
FLUSHALL:删除当前连接所有数据库的所有 key 。

场景二:删除所有满足匹配条件的 key( key 数量较少或者测试环境)

可以在命令行环境下使用 redis-cli 命令在外部执行 KEYS {pattern} 命令,拿到结果以后通过 xargs 命令传递给 DEL 作为输入参数,进而删除匹配的 key 。具体命令如下:

redis-cli -h {hostname} -p {port} -a {password} -n {database} --raw keys "{pattern}" | xargs -I {} redis-cli -h {hostname} -p {port} -a {password} -n {database} DEL "{}"

说明

  1. redis-cli 是访问 Redis 的客户端命令,用法是:redis-cli [OPTIONS] [cmd [arg [arg ...]]]
  2. hostname:服务器主机,port:服务器端口,a:密码(无密码可缺省),n:数据库编号。
  3. 低版本的 Redis 会在返回结果中加上数字编号,使用 --raw 参数可以去掉结果编号。
  4. xargs -I {} 参数可以避免 key 中存在空格导致的参数拆分异常问题

但是这种操作是有限制的,主要受限于 KEYS 命令。因为 Redis 6 版本以下都是采用单线程处理请求,如果在 key 数量较大的情况下使用 KEYS 命令,会阻塞线程,导致其他客户端无法正常访问,这在生产环境是不可接受的(基于此原因,很多公司生产环境 KEYS 都是禁用的)。这就是接下来要说的第三种场景。

场景三:删除所有满足匹配条件的 key( key 数量较多或者生产环境)

为了解决场景二中的 KEYS 命令造成的线程阻塞问题,我们可以使用 SCAN 命令来解决。

让我们先来了解一下 SCAN 命令的使用。

SCAN 用于迭代当前数据库中的数据库键,用法如下:

SCAN cursor [MATCH pattern] [COUNT count]

简单概括一下: SCAN 命令就是通过游标的方式分步从数据库获取数据,每次以游标方式进行遍历(游标从上一次遍历结果中返回,初始游标为 0 ),结果会返回一个新游标和匹配的键集合(返回键的数量不不确定,小于等于 COUNT ),如果返回游标为 0 则视为遍历结束(不以遍历结果为空作为结束标识)。可以使用 MATCH 参数匹配模式,COUNT 参数限制返回的键的个数。

KEYS 命令相比,SCAN 命令虽然复杂度也是 O(n),但是它是通过游标分步进行的,不会阻塞线程。同时 redis-cli 命令本身支持 --scan--pattern 的参数,可以直接在命令行获取到匹配的结果。

修改后的命令如下:

redis-cli -h {hostname} -p {port} -a {password} -n {database} --raw --scan --pattern "{pattern}" | xargs -I {} redis-cli -h {hostname} -p {port} -a {password} -n {database} -L 100 DEL "{}"

注意这里并没有直接在 redis-cli 中使用 SCAN 命令,而是用 --scan 参数的方式调用,这是因为 SCAN 命令会返回两个参数(游标和结果),这不利于作为 xargs 的输入参数,而 --scan 参数只返回匹配的键,可以和 xargs 命令完美结合。而且可以给 xargs 指定输入参数的条数(-L),进一步限制每一次删除的键的个数(在没有指定 COUNT 参数情况下,默认值是 10 ,SCAN 每次会返回最多 10 个左右的数据,并非严格相等)。

到这里看似问题已经得到了解决,但是在实际场景中这种处理方式还是存在一些问题。

SCAN 命令虽然解决了线程阻塞的问题,但是也带来了效率的问题。假设数据库 key 的数量级在 10w+ 左右,需要删除的 key 数量级在 100+ 左右,这时候如果使用上述命令手动操作的话无疑是十分痛苦的,需要不断地重复执行( SCAN 命令并不是每次都可以返回匹配到的结果集,只要没有返回 0 游标,就需要继续遍历)。另外一次给 DEL 传递过多的参数也不是一种很好的选择,因为如果 key 比较大时,使用 DEL 删除本身也会造成线程阻塞,这样整个命令的阻塞时间就取决于 key 的数量和大小。

综上所述,主要有两个影响因素需要考虑:一个是如何在不阻塞线程的情况下,高效查询匹配的 key ;另一个是如何避免在执行删除操作的时候造成线程阻塞。

针对第一种情况,可以考虑通过脚本程序执行 SCAN 命令,这样就不必担心重复执行的效率问题了。针对 DEL 命令可能造成的阻塞问题,可以使用 EXPIRE 命令替换。以 PHP 语言为例,可以使用以下脚本进行处理:

use Predis\Client;

$client = new Client();
while (1) {
    list($iterator, $result) = $client->scan(0, ['MATCH' => 'PHP*', 'COUNT' => 50]);
    foreach ($result as $key) {
        $client->expire($key, mt_rand(0, 600));
    }
    if ($iterator == "0") {
        break;
    }
}

说明:

  1. 这里使用的是 predis/predis composer 包,安装方式:composer require predis/predis
  2. 不使用遍历返回的结果集作为 while 的判断条件是因为 SCAN 命令结束的标志是返回值为 0 的游标。
  3. 使用 EXPIRE 设置过期的时候,过期时间采用了随机数的方式,是为了防止在删除 key 的数量过多时,同一时间集中过期引起雪崩现象。

当然,如果需要删除的 key 和 key 的总数数量级相差太大的话,使用 SCAN 命令遍历的效率还是差了些。这时可以借助 BGSAVE 生成 rdb 文件,然后再通过 rdb分析工具(rdbtools) 获取需要操作的 key ,借助程序进行过期处理。这个小技巧我们会在介绍「rdb文件」的应用场景的时候单独介绍。

本作品采用《CC 协议》,转载必须注明作者和本文链接
你应该了解真相,真相会让你自由。
本帖由系统于 8个月前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 6

收藏 关注 点赞来一波

10个月前 评论

讲的还不错,很实用。

10个月前 评论

非常好 :+1:

10个月前 评论

:+1: :+1: :+1: :+1: :+1:

10个月前 评论

:+1: :+1: :+1:

10个月前 评论

很棒,最近一直在找这个解决办法。

10个月前 评论

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