如何使用 StackExchange.Redis 使多个操作原子化

How to make multiple operations atomic using StackExchange.Redis

您好,我遇到了以下问题:

我有一个包含 strings.This 散列的散列将被多个用户查询。

当用户自带一个Key首先检查它是否存在于这个哈希中,如果不存在,添加它。

如何使操作 "check if hash exists"、"add if not exists" 成为原子操作? 阅读 redis 文档似乎 Watch 是我需要的。基本上开始一个事务,如果变量改变就结束它。

我尝试使用 Condition.HashNotExists 无济于事:

class Program {

        public static async Task<bool> LockFileForEditAsync(int fileId) {
            var database = ConnectionMultiplexer.Connect(CON).GetDatabase();
            var exists = await database.HashExistsAsync("files", fileId); //this line is for  shorting the transaction if hash exists 

            if (exists) {
                return false;
            }

            var tran = database.CreateTransaction();
            tran.AddCondition(Condition.HashNotExists("files", fileId));
            var setKey = tran.HashSetAsync("files", new HashEntry[] { new HashEntry(fileId, 1) });
            var existsTsc = tran.HashExistsAsync("files", fileId);

            if (!await tran.ExecuteAsync()) {
                return false;
            }

            var rezult = await existsTsc;
            return rezult;
        }

        public const string CON = "127.0.0.1:6379,ssl=False,allowAdmin=True,abortConnect=False,defaultDatabase=0";

        static async Task Main(string[] args) {
            int fid = 1;
            var locked = await LockFileForEditAsync(fid);
        }
    }

如果我通过 redis-cli 连接并在 cli 中发出:hset files {fileId} 1,对 BEFORE 我发出 ExecuteAsync (在调试器中)由于我放置了 Condition,所以我预计此事务会失败。然而它并没有发生。

我怎样才能基本上使用 redis 命令在两个操作上放置类似锁的东西:

  1. 检查 hashentry 是否存在
  2. 添加 hashentry

坏消息,好消息,以及其他一切...

坏消息

这不起作用,因为 SE.Redis 对事务进行了管道处理。这意味着当调用 ExecuteAsync() 时,所有事务命令都会同时发送到服务器。但是,首先评估条件。

tran.AddCondition(Condition.HashNotExists("files", fileId)); 转换为:

WATCH "files"
HEXISTS "files" "1"

并且在调用ExecuteAsync()判断条件时,这两个命令最先发送。如果条件成立(HEXISTS "files" "1" = 0),则发送其余的交易命令。

这有效地确保没有误报,因为如果 files 键在两者之间被修改(而 SE.Redis 正在评估条件),WATCH将使交易失败。

问题是漏报。例如,如果在 SE.Redis 评估条件时设置了哈希的另一个字段,交易也会失败。 WATCH "files" 做到了。

我在调用 ExecuteAsync() 时通过 redis-benchmark -c 5 HSET files 2 1 运行 进行了测试。条件通过,但是虽然字段“1”不存在,但交易失败,因为字段“2”设置在两者之间。

我在单独的 redis-cli window 中使用 MONITOR 命令进行了验证。这对于排除未满足的期望或简单地查看真正进入服务器的内容和时间是很方便的。

A WATCH 在您关心散列的某个字段时没有帮助,因为它会在触及其他字段时产生漏报。

解决此问题的方法是改用常规密钥 (files:1)。

好消息

有一个命令可以完全按照您的意愿执行:HSETNX

这完全简化了 LockFileForEditAsync():

    public static async Task<bool> LockFileForEditAsync(int fileId)
    {
        var database = ConnectionMultiplexer.Connect(CON).GetDatabase();
        var setKey = await database.HashSetAsync("files", fileId, 1, When.NotExists);
        return setKey;
    }

注意 When.NotExists 参数。它导致发送 HSETNX "files" "1" "1" 命令。

对于其他一切...

您可以使用 Lua scripts in situations like this, where you want to have some conditional action done atomically, like in .

看来你是想做分布式锁。请参阅 Distributed locks with Redis for other aspects you may want to consider. See 了解关于这方面的精彩故事。