predis SCAN 比 KEYS 慢

问题描述

我需要一种通过前缀获取所有键以删除它们的方法

我读过有关 KEYS 不适合生产的信息,因此我进行了一些测试以检查性能。我使用 predis 1.1.6 (PHP),我在本地机器和使用 elasticache redis 的测试 AWS 环境中进行了测试。我在一个包含大约 30 万个项目的节点上执行此操作。

我使用前缀:CLIENT/ID_CLIENT/MODULE:HASH 翻译成 client/9999/products:452a269b82c199ef27f5a299e3b0f98531216ccf

所以我需要从客户端和模块中搜索删除所有键。 由于我使用前缀,因此我设置了正确的前缀并使用了 predis方法

$this->_redisPrefix('client/9999/products:');
$keys = $this->_redis_client->keys('*');

这非常快,大约需要 50 毫秒。

由于不建议在生产中使用 KEYS,因此我尝试使用 SCAN 实现相同的目标。 predis 没有扫描方法,所以我需要这样做:

foreach (new Iterator\Keyspace($this->_redis_client,'client/9999/products:*') as $key) {
    $keys[] = $key;
}

这会返回完全相同的结果,但花了 20 秒(!)。我认为这与我的本地机器有关,但我已将其部署到我们的 aws 环境中,并且响应时间相同。我没有使用分页,因为我需要删除所有项目,我不知道有多少。可以是 10 也可以是 1000(或更多)

我想避免使用 KEYS,但我不能在这种时间下使用 SCAN。

解决方法

在生产中使用 KEYS

首先了解为什么不应该在生产中使用 KEYS 很重要。

KEYS 的时间复杂度为 O(N),其中 N 是整个数据库的元素数量。不是有多少满足模式。由于只能同时运行一个命令(Redis 不是多线程的),因此其他所有操作都必须等待该 KEYS 完成。

见:Why KEYS is advised not to be used in Redis?

根据文档:

虽然此操作的时间复杂度为 O(N),但常数时间相当低。例如,运行在入门级笔记本电脑上的 Redis 可以在 40 毫秒内扫描 100 万个密钥数据库。

警告:将 KEYS 视为仅应在生产环境中极其谨慎地使用的命令。当它针对大型数据库执行时,它可能会破坏性能。此命令用于调试和特殊操作,例如更改键空间布局。不要在常规应用程序代码中使用 KEYS。如果您正在寻找一种在键空间子集中查找键的方法,请考虑使用 SCAN 或集合。

这表明如果您的记录少于一百万,使用 keys 应该没问题。但随着您的数据库增长,或者您有更多的并发用户,可能会出现问题。

KEYS 的替代方案

扫描

KEYS 的常见替代方法是 SCAN(这是您正在使用的)。请注意,这仍然是一个糟糕的选择,因为它与 KEYS 非常相似,只是结果以块的形式返回,并且具有 O(N),其中 N 是整个数据库的元素数。>

优点是它不会阻塞服务器,但它具有相同的时间复杂度KEYS。事实上,如果您只想得到结果,并且不关心阻塞数据库,那么它可能比 KEYS 慢,因为它必须执行多个查询(如您所见)。

HSET

更好的选择是使用 HSET。

当您想将元素放入 HSET 时,请使用:

HSET client/9999/products "id_547" "Book"
HSET client/9999/products "whatever_key_you_want" "Laptop"
$this->_redis_client->hset('client/9999/products','id_547','Book');
$this->_redis_client->hset('client/9999/products','whatever_key_you_want','Laptop');

当你想获得所有的密钥时,只需使用 HKEYS:

HKEYS client/9999/products
1) id_547
2) whatever_key_you_want
$this->_redis_client->hkeys('client/9999/products')

与 KEYS 不同,HKEYS 的复杂度是 O(N),其中 N 是散列的大小(不是整个数据库的大小)。

如果键变得非常大,您可能需要使用 HSCAN

性能测试

在一个大约有 2,000,000 个项目的 redis 数据库中:

for ($i = 0; $i <= 100; $i++) {
    $client->set("a:{$i}","value{$i}");
}
for ($i = 0; $i <= 100; $i++) {
    $client->hset("b",$i,"value{$i}");
}

测试 1:按键

$start = microtime(true);
var_dump(count($client->keys('a:*')));
$end = microtime(TRUE);
echo ($end - $start) . "s\n";

测试 2:扫描

$start = microtime(true);
$count = 0;
foreach (new Keyspace($client,'a:*') as $key) {
    $count++;
}
$end = microtime(TRUE);
echo ($end - $start) . "s\n";

测试 3:HKEYS

$start = microtime(true);
var_dump(count($client->hkeys('b')));
$end = microtime(TRUE);
echo ($end - $start) . "s\n";

结果

  • 按键:~0.21s
  • 扫描:约 20 秒
  • HKEYS:~0.01s

如您所见,HKEYS 的速度要快得多,并且不受数据库大小的影响。

我还建议使用 redis PECL 扩展而不是 predis:

我得到了 Redis 扩展:

  • 按键:~0.21s(变化不大)
  • 扫描:约 17 秒(小幅增加)
  • HKEYS:~0.0004s(更快!)