Лучший способ предотвратить условия гонки в многопользовательской веб-среде?

Скажем, у вас есть действие в ASP.NET MVC в среде с несколькими экземплярами, которая выглядит примерно так: *:

public void AddLolCat(int userId)
{
    var user = _Db.Users.ById(userId);

    user.LolCats.Add( new LolCat() );

    user.LolCatCount = user.LolCats.Count();

    _Db.SaveChanges();
}

Когда пользователь несколько раз нажимает кнопку или обновляется, условия гонки будут происходить, что позволяет LolCatCount не похож на количество LolCats.

Вопрос

Каков общий способ устранения этих проблем? Вы можете исправить ее на стороне клиента в JavaScript, но это может быть не всегда возможно. То есть когда что-то происходит на странице обновления, или потому, что кто-то висит вокруг в Fiddler.

  • Думаю, вам нужно сделать какой-то сетевой замок?
  • Вам действительно нужно страдать дополнительной задержкой за звонок?
  • Можете ли вы сказать, что действие разрешено выполнять только один раз на пользователя?
  • Есть ли какой-нибудь общий шаблон, который вы можете использовать? Как фильтр или атрибут?
  • Вы вернетесь рано или действительно заблокируете процесс?
  • Когда вы вернетесь раньше, существует ли "установленный" ответ/ответный код, который я должен вернуть?
  • Когда вы используете блокировку, как вы предотвращаете головоломку потоков с (полу) длительными процессами?

* просто тупой пример, показанный для краткости. Примеры реальных стран намного сложнее.

Ответ 1

Ответ 1: (общий подход)
Если хранилище данных поддерживает транзакции, вы можете сделать следующее:

using(var trans = new TransactionScope(.., ..Serializable..)) {
    var user = _Db.Users.ById(userId);
    user.LolCats.Add( new LolCat() );
    user.LolCatCount = user.LolCats.Count();
    _Db.SaveChanges();
    trans.Complete();
}

это заблокирует запись пользователя в базе данных, и другие запросы ожидают, пока транзакция не будет выполнена.

Ответ 2: (возможен только с одним процессом)
Включение сеансов и использование сеанса вызовет неявное блокирование между запросами одного и того же пользователя (сеанса).

Session["TRIGGER_LOCKING"] = true;

Ответ 3: (пример)
Выведите количество LolCats из коллекции, а не отслеживайте ее в отдельном поле и, таким образом, избегайте проблем с несогласованностью.

Ответы на ваши конкретные запросы:

  • Думаю, вам нужно сделать какой-то сетевой замок?
    да, блокировки базы данных являются общими.

  • Вам действительно нужно страдать дополнительной задержкой за звонок?
    скажите что?

  • Можете ли вы сообщить Action, что разрешено выполнение только один раз для пользователя

    Вы можете реализовать атрибут, который использует неявную блокировку сеанса или какой-то пользовательский вариант, но который не будет работать между процессами.

  • Существует ли какая-либо распространенная модель, которую вы можете использовать? Как фильтр или атрибут?
    Обычная практика заключается в использовании блокировок в базе данных для решения проблемы с несколькими экземплярами. Нет фильтра или атрибута, о котором я знаю.

  • Вы вернетесь рано или действительно заблокируете процесс?
    Зависит от вашего варианта использования. Обычно вы ждете ( "заблокируйте процесс" ). Однако, если ваше хранилище базы данных поддерживает шаблон async/await, вы сделаете что-то вроде

    var user = await _Db.Users.ByIdAsync(userId);
    

    это освободит поток для выполнения другой работы в ожидании блокировки.

  • Когда вы вернетесь раньше, существует ли "установленный" ответ/ответный код, который я должен вернуть?
    Я так не думаю, выберите что-то, что подходит вашему прецеденту.

  • Когда вы используете блокировку, как вы предотвращаете головоломку потоков с (полу) длительными процессами?
    Думаю, вам стоит рассмотреть возможность использования очередей.

Ответ 2

В "multi-instance" вы, очевидно, ссылаетесь на веб-ферму или, возможно, на ситуацию в веб-саду, где просто использование мьютекса или монитора не будет достаточным для сериализации запросов.

Итак... у вас есть только одна база данных на задней панели? Почему бы просто не использовать транзакцию базы данных?

Похоже, вы, вероятно, не хотите принудительно сериализовать доступ к этому разделу кода для всех идентификаторов пользователя, правильно? Вы хотите сериализовать запросы на идентификатор пользователя?

Мне кажется, что правильное мышление об этом заключается в сериализации доступа к исходным данным, которые являются записями LolCats в базе данных.

Мне нравится идея отключить кнопку или ссылку в браузере в течение всего времени запроса, чтобы предотвратить повторное нажатие кнопки на кнопке до того, как предыдущие запросы закончат обработку и возврат. Это кажется достаточно простым шагом с большой пользой.

Но я сомневаюсь, что этого достаточно, чтобы гарантировать сериализованный доступ, который вы хотите обеспечить.

Вы также можете реализовать разделяемое состояние сеанса и реализовать какую-то блокировку для объекта на основе сеанса, но, вероятно, для сбора парадигмы serializable-для пользователя, вероятно, должна быть коллекция (идентификаторов пользователя).

Я проголосовал бы за использование транзакции базы данных.

Ответ 3

Я предлагаю и лично использовать mutex в этом случае.

Я пишу здесь: Проблемы выпуска Mutex в коде ASP.NET С#, класс, который обрабатывает мьютекс, но вы можете сделать свой собственный.

Таким образом, основываясь на классе из этого ответа, ваш код будет выглядеть следующим образом:

public void AddLolCat(int userId)
{
    // I add here some text in front of the number, because I see its an integer
    //  so its better to make it a little more complex to avoid conflicts
    var gl = new MyNamedLock("SiteName." + userId.ToString());
    try
    {
        //Enter lock
        if (gl.enterLockWithTimeout())
        {
            var user = _Db.Users.ById(userId);    
            user.LolCats.Add( new LolCat() );    
            user.LolCatCount = user.LolCats.Count();    
            _Db.SaveChanges();
        }
        else
        {
            // log the error
            throw new Exception("Failed to enter lock");
        }
    }
    finally
    {
        //Leave lock
        gl.leaveLock();
    }
}

Здесь блокировка основывается на пользователе, поэтому разные пользователи не будут блокировать друг друга.

О блокировке сеанса

Если вы используете сеанс asp.net для своего вызова, вы можете выиграть "билет" бесплатного блокировки из сеанса. Сеанс блокирует каждый вызов до тех пор, пока страница не вернется.

Прочитайте об этом на этом q/a:
Веб-приложение заблокировано при обработке другого веб-приложения при совместном использовании того же сеанса
Предоставляет ли веб-формы ASP.NET возможность двойного клика? jQuery Ajax вызовы для веб-службы кажутся синхронными

Ответ 4

Ну MVC stateless означает, что вам придется вручную обрабатывать себя. С точки зрения пуриста я бы рекомендовал предотвратить множественные нажатия, используя клиентскую блокировку, хотя я предпочитаю отключить эту кнопку и применить соответствующий CSSClass, чтобы продемонстрировать ее отключенную государство. Я думаю, что мои рассуждения состоят в том, что мы не можем полностью определить потребителя действия, поэтому, пока вы приводите пример Fiddler, нет никакого способа действительно определить, применимы ли множественные клики или нет.

Однако, если вы хотите использовать механизм блокировки на стороне сервера, эта статья предоставляет пример хранения информации запрашивающего в сервере, и возвращает соответствующий ответ в зависимости от тайм-аута/действий, которые вы хотите реализовать.

НТН

Ответ 5

Одним из возможных решений является избежание избыточности, которая может привести к несогласованным данным. т.е. если LolCatCount можно определить во время выполнения, затем определите его во время выполнения, вместо того чтобы продолжать эту избыточную информацию.