使用 EVAL、SCAN 和 DEL 的 Redis 通配符删除脚本 returns "Write commands not allowed after non deterministic commands"

Redis wildcard delete script using EVAL, SCAN, and DEL returns "Write commands not allowed after non deterministic commands"

所以我正在寻求构建一个 lua 脚本,该脚本使用 SCAN 根据模式查找键并(原子地)删除它们。我首先准备了以下脚本

local keys = {};
local done = false;
local cursor = "0"
repeat
    local result = redis.call("SCAN", cursor, "match", ARGV[1], "count", ARGV[2])
    cursor = result[1];
    keys = result[2];
    for i, key in ipairs(keys) do
        redis.call("DEL", key);
    end
    if cursor == "0" then
        done = true;
    end
until done
return true;

哪个会吐出下面的"Err: @user_script: 9: Write commands not allowed after non deterministic commands "所以我想了一下,想出了下面的脚本:

local all_keys = {};
local keys = {};
local done = false;
local cursor = "0"
repeat
    local result = redis.call("SCAN", cursor, "match", ARGV[1], "count", ARGV[2])
    cursor = result[1];
    keys = result[2];
    for i, key in ipairs(keys) do
        all_keys[#all_keys+1] = key;
    end
    if cursor == "0" then
        done = true;
    end
until done
for i, key in ipairs(all_keys) do
    redis.call("DEL", key);
end
return true;

仍然returns同样的错误(@user_script: 17: 在非确定性命令之后不允许写入命令)。这让我难住了。有什么办法可以规避这个问题吗?

脚本是 运行 使用 phpredis 和以下

$args_arr = [
          0 => 'test*',   //pattern
          1 => 100,     //count for SCAN
  ];
  var_dump($redis->eval($script, $args_arr, 0));

更新: 以下内容适用于 3.2 以下的 Redis 版本。从那个版本开始,基于效果的复制取消了对非确定性的禁令,所以所有的赌注都关闭了(或者更确切地说,打开了)。

您不能(也不应该)将 SCAN 系列命令与脚本中的任何写入命令混合使用,因为前者的回复取决于内部 Redis 数据结构,而这些数据结构又是服务器进程独有。换句话说,两个 Redis 进程(例如主进程和从进程)不能保证 return 相同的回复(因此在 Redis 复制上下文中 [这不是基于操作的,而是基于语句的] 会破坏它)。

如果在随机命令(例如 SCAN 以及 TIME 之后执行任何写入命令(例如 DEL),Redis 会尝试通过阻止任何写入命令(例如 DEL)来保护自己免受此类情况的影响。 SRANDMEMBER 和类似)。我敢肯定有办法解决这个问题,但你愿意这样做吗?请记住,您将进入系统行为未定义的未知领域。

相反,接受您不应该混合随机读取和写入这一事实,并尝试考虑一种不同的方法来解决您的问题,即以原子方式根据模式删除一堆密钥。

首先问问自己是否可以放宽任何要求。它必须是原子的吗?原子性意味着 Redis 将在删除期间被阻塞(无论最终实现如何)并且操作的长度取决于作业的大小(即被删除的键的数量及其内容[删除一个大集合是例如,比删除短字符串更昂贵])。

如果原子性不是必须的,periodically/lazily SCAN 并小批量删除。如果这是必须的,请理​​解您基本上是在尝试模仿 evil KEYS 命令 :) 但是如果您事先了解该模式,您可以做得更好。

假设模式在您的应用程序 运行 时间内已知,您可以收集相关键(例如在一个集合中),然后使用该集合以原子和复制安全的方式实现删除与遍历整个键空间相比,这更有效。

然而,最"difficult"的问题是,如果您需要运行在确保原子性的同时进行临时模式匹配。如果是这样,问题归结为获取键空间的按模式过滤的快照,然后立即进行一系列删除(再次强调:当数据库被阻塞时)。在那种情况下,您可以在 Lua 脚本中很好地使用 KEYS 并希望获得最好的结果......(但非常清楚您可能会很快求助于 SHUTDOWN NOSAVE :P)。

最后的优化是索引键空间本身。 SCANKEYS 基本上都是完整的 table 扫描,那么如果我们要索引那个 table 呢?想象一下在事务期间可以查询的键名称上保留一个索引——您可能可以使用排序集和字典范围 (HT @TwBert) 来消除大部分模式匹配需要。但是成本很高……您不仅会进行双重簿记(将每个键的名称成本存储在 RAM 和 CPU 中),而且还被迫增加应用程序的复杂性。为什么要增加复杂性?因为要实现这样的索引,您必须自己在应用程序层(以及可能所有其他 Lua 脚本)中维护它,小心地将每个写入 Redis 的操作包装在一个也会更新索引的事务中。

假设您做了所有这些(并考虑到明显的缺陷,例如增加的复杂性可能导致错误,Redis、RAM 和 CPU 上的写入负载至少增加一倍,缩放限制等。 ..) 您可以拍拍自己的肩膀并祝贺自己以一种非设计的方式使用 Redis。虽然即将推出的 Redis 版本可能(或可能不)包括针对这一挑战的更好解决方案(@TwBert - 想要联合 RCP/contrib 并再次破解 Redis 吗?),在尝试之前,我强烈建议您重新考虑最初的要求并验证您是否正确使用了 Redis(即根据您的数据访问需求设计 "schema")。