问题 使用EVAL,SCAN和DEL的Redis通配符删除脚本返回“非确定性命令后不允许写入命令”


所以我正在寻求构建一个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:非确定性命令后不允许写入命令”所以我想了一下,想出了以下脚本:

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;

仍然返回相同的错误(@user_script:17:非确定性命令后不允许写入命令)。这让我很难过。有没有办法绕过这个问题?

脚本是使用phpredis和以下运行的

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

4569
2018-01-16 02:07


起源

这个错误对我来说似乎是对的。 SCAN可以在另一台redis服务器上返回不同的密钥,因此您的脚本会破坏redis复制。你正在做的也是不好的做法,因为你的脚本可能会运行很长时间(> 10-50ms)。阅读SLOWLOG。您必须在客户端上执行SCAN,并将批处理命令(即msgpack数组)发送到执行实际删除的Lua脚本。由于SLOWLOG < - >并发,不要试图以原子方式执行此操作。 - Tw Bert
>这个错误对我来说似乎是正确的。 SCAN可以在另一台redis服务器上返回不同的密钥,因此您的脚本会破坏redis复制。我不确定我理解这个推理。就我写的第一个脚本而言,我有一个类似的想法,也许它不高兴光标没有完全发挥作用,所以我把它修改为第二个。如果你说的仍然适用于第二个脚本,那么SCAN作为命令似乎完全没有价值。 - Billy
>你正在做的也是不好的做法,因为你的脚本可以运行很长时间(> 10-50ms)。我只计划每月或每周运行一次脚本。 - Billy
>您必须在客户端上执行SCAN,并将批处理命令(即msgpack数组)发送到执行实际删除的Lua脚本。难道这不会导致SCAN之间插入的某些“新”键被扫描捕获而有些未被触及吗? - Billy
(1)Itamar非常好地解释了这一点。我只能说,错误信息真的会阻止你做错事。很高兴它就在那里。 (2)好吧,如果没问题,那没问题。但是那时你每周只会“出现一次”并发问题。 没有 可以介于两者之间。它打破了redis原则imho。 (3)好点,视情况而定。但是你总是可以用0来启动SCAN。 - Tw Bert


答案:


更新: 以下适用于最高3.2的Redis版本。从那个版本开始,基于效果的复制解除了对非决定论的禁令,所以所有的赌注都是关闭的(或者更确切地说是开启)。

你不能(也不应该)混合 SCAN 在脚本中具有任何写入命令的命令族,因为前者的回复依赖于内部Redis数据结构,而这些数据结构又是服务器进程所特有的。换句话说,两个Redis进程(例如主进程和从进程)不能保证返回相同的回复(因此在Redis复制上下文中[这不是操作 - 但基于语句]会破坏它)。

Redis试图通过阻止任何写命令来保护自己免受此类情况的影响(例如 DEL如果它是在随机命令之后执行的(例如, SCAN 但也 TIMESRANDMEMBER 和类似的)。我确定有办法解决这个问题,但你想这样做吗?请记住,您将进入未定义系统行为的未知区域。

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

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

如果原子性不是必须的,定期/懒惰 SCAN 并小批量删除。如果这是必须的,要明白你基本上是在试图效仿 邪恶 KEYS 命令:)但如果您事先了解该模式,您可以做得更好。

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

但是,最困难的问题是如果您需要在确保原子性的同时运行ad-hoc模式匹配。如果是这样,问题归结为获得密钥空间的按模式过滤的快照,紧接着是一系列删除(重新强调:数据库被阻止时)。在这种情况下,你可以很好地使用 KEYS在你的Lua剧本中,并希望最好的...(但完全了解你可以诉诸 SHUTDOWN NOSAVE 很快:P)。

Last Optimization用于索引键空间本身。都 SCAN 和 KEYS 基本上都是全表扫描,那么如果我们要对该表进行索引呢?想象一下,在事务中可以查询的键名称上保留一个索引 - 你可以使用一个排序集和词典范围(HT) @TwBert)消除大多数模式匹配需求。但是成本很高......你不仅会进行双重记账(将每个密钥的名称成本存储在RAM和CPU中),还会被迫增加应用程序的复杂性。为何增加复杂性?因为要实现这样的索引,您必须自己在应用程序层(可能还有所有其他Lua脚本)中维护它,在每次更新索引的事务中小心地将每个写入操作包装到Redis。

假设你做了所有这些(并考虑到明显的陷阱,比如增加了复杂性的漏洞潜力,至少加倍Redis,RAM和CPU上的写入负载,缩放等限制......)你可以拍拍自己肩并祝贺自己以不适合的方式使用Redis。虽然即将推出的Redis版本可能(或可能不)包含更好的解决方案来应对这一挑战(@TwBert - 想要联合RCP / contrib并再次破解Redis一点?),在尝试此操作之前,我真的建议您重新考虑原始要求并验证您是否正确使用Redis(即根据您的数据访问需求设计“架构”)。


12
2018-01-16 18:20



好吧,我曾经看到浮动的bash脚本使用SCAN过期或del键,并且他们的创建者想要尝试使它们成为原子,所以我想我会捅它。如果它工作或许我可以使用它。如果我真的不应该,那么我想我不会。我也看到人们提到他们将所有数据保存在一个大表中而不是将其分成单独的表,但我不确定如何在不保留额外开销的情况下维护大量数据(或者我似乎从你的帖子中使用Redis以一种不是为其设计的方式 - Billy
@ItamarHaber:我选择退出;)忙着忙,我们不需要这个atm。我想我们以一种专为:)设计的方式使用Redis。很棒的帖子顺便说一句。 - Tw Bert
Ty Tw - 我实际上认为The Last Optimization并不是一个坏主意......也许我会在邮件列表中追求它。 - Itamar Haber