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

收藏 关注 点赞来一波

11个月前 评论

讲的还不错,很实用。

11个月前 评论

非常好 :+1:

11个月前 评论

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

11个月前 评论

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

11个月前 评论

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

11个月前 评论

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