Как сделать несколько операций атомарными с помощью StackExchange.Redis

Здравствуйте, у меня следующая проблема:

У меня есть хэш, содержащий строки. Этот хеш будет запрашиваться несколькими пользователями.

Когда пользователь приходит с Key, чтобы сначала проверить, существует ли он в этом хэше, а если нет, добавьте его.

Как сделать операции «проверка, существует ли хеш», «добавить, если не существует» атомарными? Читая документацию по 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, правильно ПЕРЕД, я выдаю ExecuteAsync (в отладчике), я ожидаю, что эта транзакция завершится неудачно, так как я разместил Condition. Однако этого не происходит.

Как я могу в основном использовать команды redis, чтобы установить что-то вроде блокировки двух операций:

  1. Проверьте, существует ли хеш-код
  2. Добавить хешенцию

person Bercovici Adrian    schedule 29.01.2020    source источник


Ответы (1)


Плохие новости, хорошие новости и все остальное ...

Плохие новости

Это не работает, потому что 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" делает это так.

Я проверил это, запустив redis-benchmark -c 5 HSET files 2 1 при вызове ExecuteAsync(). Условие выполнено, но транзакция завершилась неудачно, хотя поле «1» не существовало, потому что поле «2» было установлено между ними.

Я проверил, используя команду MONITOR в отдельном окне redis-cli. Это удобно для устранения неполадок, которые не оправдались, или просто для того, чтобы увидеть, что на самом деле происходит на сервере и когда.

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 в подобных ситуациях, когда вы хотите, чтобы некоторые условные действия выполнялись атомарно, например в как использовать команду spop с count, если в наборе есть такое количество (count) элементов.

Похоже, вы пытаетесь выполнить распределенную блокировку. См. Распределенные блокировки с Redis, чтобы узнать о других аспектах, которые вы, возможно, захотите рассмотреть. См. Что такое распределенная атомная блокировка в драйверах кешей? за хороший рассказ об этом.

person LeoMurillo    schedule 31.01.2020
comment
По-видимому, это работает с моим вариантом. Я не хочу просто добавлять, если не существует. Я хочу добавить и вернуть истину (если она не существует и была создана успешно) и ложь, если она существует, или она пыталась ее создать но кто-то тем временем создал. (транзакция) - person Bercovici Adrian; 31.01.2020
comment
Это именно то, что делает HSETNX: устанавливает поле в хэше, хранящемся по ключу, в значение, только если поле еще не существует. Если ключ не существует, создается новый ключ, содержащий хеш. Если поле уже существует, эта операция не действует.. Ваше последнее условие, которое он пытался создать, но кто-то создал тем временем. (Транзакция) не применяется, если вы используете одну команду, Redis является однопоточным, поэтому одна команда автоматически атомарна. - person LeoMurillo; 31.01.2020
comment
См. Возвращаемые значения для HSETNX: 1, если field является новым полем в хэше и значение было установлено. 0, если поле уже существует в хэше и никакая операция не выполнялась. Я не понимаю, почему это не совсем соответствует тому, что вы хотите :-) Какой вариант вы имеете в виду? - person LeoMurillo; 31.01.2020
comment
Вы действительно правы. Я не видел перегрузки HashSetAsync When. Это действительно заменяет всю мою логику (хотя она работала: D - транзакция + условие) - person Bercovici Adrian; 31.01.2020
comment
Я воспроизвел вашу проблему, скопировав / вставив ваш код, и мог видеть команду WATCH, отправленную до Execute, поэтому не уверен, как вы заставили транзакцию работать. Но да, HSETNX сделает трюк намного проще :-) - person LeoMurillo; 31.01.2020
comment
Что ж, в моем коде вы можете видеть, что каждый пользователь сначала пытается увидеть, существует ли хеш, а затем, если нет, добавляет его. В случае, когда пользователь 1 проверяет хеш и добивается успеха, прямо перед записью хеша пользователь 2 проверяет хеш и тоже успешно ( пользователь 1 еще не написал), то транзакция пользователя 2 имеет условие, которое завершится ошибкой, поскольку пользователь 1 написал в этот момент. - person Bercovici Adrian; 31.01.2020
comment
Обновлен ответ, действительно, он работает, если другой клиент устанавливает поле 1, но не работает (ложно отрицательный), если другой клиент устанавливает другое поле. Спасибо, что поделились, заставили меня копнуть глубже, и я узнал больше - person LeoMurillo; 31.01.2020
comment
Позвольте нам продолжить это обсуждение в чате. - person Bercovici Adrian; 31.01.2020