所以我正在寻求构建一个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
系列命令与脚本中的任何 write 命令混合使用,因为前者的回复依赖于内部 Redis 数据结构,而内部 Redis 数据结构又是服务器进程独有的。换句话说,两个 Redis 进程(例如主进程和从进程)不能保证返回相同的回复(因此在 Redis 复制上下文中 [不是操作而是基于语句] 会破坏它)。
Redis 试图通过阻止任何写入命令(例如 DEL
)来保护自己免受此类情况的影响,如果它是在随机命令之后执行的(例如 SCAN
,但也TIME
,SRANDMEMBER
和类似)。我相信有办法解决这个问题,但你想这样做吗?请记住,您将进入未定义系统行为的未知领域。
相反,接受这样一个事实,即你不应该混合随机读写,并尝试想出一种不同的方法来解决问题,即根据原子方式的模式删除一堆键。
首先问问自己是否可以放宽任何要求。它必须是原子的吗?原子性意味着 Redis 将在删除期间被阻止(无论最终实现如何),并且操作的长度取决于作业的大小(即删除的键数及其内容 [例如,删除大型集合比删除短字符串更昂贵])。
如果原子性不是必须的,请定期/懒惰地小批量SCAN
和删除。如果这是必须的,请理解您基本上是在尝试模仿邪恶的KEYS
命令:)但是,如果您对模式有先验知识,则可以做得更好。
假设模式在应用程序运行时是已知的,则可以收集相关键(例如,在 Set 中),然后使用该集合以原子和复制安全的方式实现删除,这比遍历整个键空间更有效。
但是,最"困难"的问题是是否需要在确保原子性的同时运行临时模式匹配。如果是这样,问题归结为获取密钥空间的按模式过滤的快照,然后立即进行一系列删除(再次强调:在数据库被阻止时)。在这种情况下,您可以在Lua脚本中很好地使用KEYS
,并希望得到最好的结果......(但非常清楚你可能会很快求助于SHUTDOWN NOSAVE
:P)。
最后的优化是索引键空间本身。SCAN
和KEYS
基本上都是全表扫描,那么如果我们要索引该表呢?想象一下,在事务期间可以查询的键名称上保留索引 - 您可能可以使用排序集和字典编纂范围(HT @TwBert)来消除大多数模式匹配需求。但代价很大...您不仅要进行双重簿记(将每个密钥的名称成本存储在RAM和CPU中),而且还会被迫增加应用程序的复杂性。为什么要增加复杂性?因为要实现这样的索引,你必须在应用层(可能还有所有其他Lua脚本)中自己维护它,小心翼翼地将每个写入操作包装到Redis的事务中,该事务也会更新索引。
假设你做了所有这些(并考虑到明显的陷阱,如增加的复杂性,潜在的错误,Redis,RAM和CPU上的写入负载至少翻倍,扩展限制等等),你可以拍拍自己的肩膀,祝贺自己以非设计的方式使用Redis。虽然即将推出的 Redis 版本可能会(也可能不会)包含针对这一挑战的更好解决方案(@TwBert - 想要做一个联合 RCP/contrib 并再次破解 Redis 一点?),但在尝试之前,我真的敦促您重新考虑原始需求并验证您是否正确使用了 Redis(即根据您的数据访问需求设计您的"模式")。