predis SCAN 比 KEYS 慢

predis SCAN slower than 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;
}

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

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

在生产中使用 KEYS

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

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

参见:

根据文档:

While the time complexity for this operation is O(N), the constant times are fairly low. For example, Redis running on an entry level laptop can scan a 1 million key database in 40 milliseconds.

Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Don't use KEYS in your regular application code. If you're looking for a way to find keys in a subset of your keyspace, consider using SCAN or sets.

这表明如果您的记录少于一百万,使用 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
  • 扫描:~20s
  • HKEYS: ~0.01s

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

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

使用 Redis 扩展我得到:

  • KEYS:~0.21s(变化不大)
  • 扫描:~17 秒(小幅增加)
  • HKEYS:~0.0004s(快多了!)