lua - 使用EVAL、SCAN、DEL的Redis通配符删除脚本返回 "Write commands not allowed after non deterministic commands"

标签 lua 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;

这会吐出以下“错误:@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));

最佳答案

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

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

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

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

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

如果原子性不是必须的,请定期/懒惰地 KEYS 并小批量删除。如果这是必须的,请了解您基本上是在尝试模拟 邪恶的 KEYS 命令 :) 但是如果您事先了解该模式,则可以做得更好。

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

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

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

假设你做了所有这些(并考虑到明显的陷阱,比如增加的复杂性潜在的错误,Redis、RAM 和 CPU 上的写入负载至少加倍,扩展限制等等......)你可以轻拍自己肩并祝贺自己以一种它不是设计的方式使用Redis。虽然即将推出的 Redis 版本可能(也可能不)包含针对这一挑战的更好解决方案( @TwBert - 想要联合 RCP/contrib 并再次破解 Redis?),但在尝试之前,我真的敦促您重新考虑原始要求并验证您是否正确使用 Redis(即根据您的数据访问需求设计您的“模式”)。

关于lua - 使用EVAL、SCAN、DEL的Redis通配符删除脚本返回 "Write commands not allowed after non deterministic commands",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27976255/

相关文章:

node.js - 如何让 Node.js 进程相互通信

lua - 如何知道客户是否在 Awesome WM 中被杀死

c++ - Lua,我可以禁用 C++ 中的部分语言吗?

Lua 和 Alien 结构

redis - 从 Spring Session Redis 获取事件 session 数

caching - 何时在 GraphQL 服务器设置中使用 Redis 以及何时使用 DataLoader

redis - 我如何每天转储我的redis数据库?

vim - 在 Vim 中打开 Lua .love 文件

time - Lua - 当前时间(以毫秒为单位)

design-patterns - 使用Step Function同步Primay Data Source和Secondary Data Sources并根据数据更新触发Actions