Предотвращения состояния гонки, если существует-обновить-другое-вставлять в сущности рамок


Я читал другие вопросы о том, как реализовать семантику if-exists-insert-else-update в EF, но либо я не понимаю, как работают ответы, либо они на самом деле не решают проблему. Предлагаемое общее решение заключается в том, чтобы обернуть работу в область транзакции (например: Реализация if-not-exists-insert с использованием Entity Framework без условий гонки):

using (var scope = new TransactionScope()) // default isolation level is serializable
using(var context = new MyEntities())
{
    var user = context.Users.SingleOrDefault(u => u.Id == userId); // *
    if (user != null)
    {
        // update the user
        user.property = newProperty;
        context.SaveChanges();
    }
    else
    {
        user = new User
        {
             // etc
        };
        context.Users.AddObject(user);
        context.SaveChanges();
    }
}

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

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

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

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

Любой идеи?

EDIT : я попытался выполнить приведенный выше код одновременно в двух разных потоках, используя один и тот же идентификатор пользователя, и, несмотря на то, что они выполняли сериализуемые транзакции, они оба смогли одновременно войти в критический раздел ( * ). Это приводит к возникновению исключения UpdateException, когда второй поток пытается вставить тот же идентификатор пользователя, который только что вставил первый. Это происходит потому, что, как указал Ладислав ниже, сериализуемая транзакция принимает эксклюзивные блокировки только после того, как он начал изменять данные, а не читать.

4 23

4 ответа:

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

Вы хотите изоляции, где выберите команда блокирует всю таблицу исключительно для одного клиента. Он должен блокировать всю таблицу, потому что в противном случае он не будет решать параллелизм для вставки "той же самой" записи. Детальное управление блокировкой записей или таблиц с помощью команд select возможно при использовании подсказок, но для этого необходимо написать прямые SQL-запросы - EF не поддерживает их. Я описал подход для исключительной блокировки этой таблицы здесь , но это похоже на создание последовательного доступа к таблице, и это влияет на все остальные клиенты, обращающиеся к этой таблице.

Если вы действительно уверены, что эта операция выполняется только в вашем отдельном методе, и нет других приложений, использующих вашу базу данных, вы можете просто поместить код в критическую секцию (синхронизация .NET, например, с lock) и убедиться на стороне .NET, что только один поток может получить доступ к критической секции. Это не очень надежное решение, но любая игра с блокировками и уровнями транзакций оказывает большое влияние на производительность и пропускную способность базы данных. Этот подход можно сочетать с оптимистическим параллелизмом (уникальные ограничения, временные метки и т. д.).

Вы можете изменить уровень изоляции транзакций с помощью TransactionOptions для TransactionScope на более строгий (я думаю, в вашем случае это RepeatableRead или Serializable), но помните, что любые блокировки уменьшают масштабируемость.

Действительно ли важно обеспечить такой уровень контроля параллелизма? Будет ли ваше приложение использоваться в тех же случаях в производственной среде? Вот хороший пост Уди Дахана о расовых условиях.

Может быть, я что-то упускаю, но когда я моделирую приведенный выше пример в среде SQL Management Studio, это работает, как и ожидалось.

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

Предполагая, что этот идентификатор пользователя не существует, обе транзакции пытаются вставить новую запись с идентификатором пользователя - что невозможно. Из-за их Сериализуемого уровня изоляции обе транзакции не могут вставить новую запись в пользователей таблица, потому что это ввело бы фантомные чтения для другой транзакции.

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

Entity Framework обрабатывает это по-другому? Я подозреваю, что вы закончите с UpdateException с вложенным SqlException, идентифицирующим тупик.

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

Я очень просто использую исключение и сначала пытаюсь вставить. Я использую модификацию вашего исходного кода в качестве примера:

using(var context = new MyEntities())
{
    EntityEntry entityUser = null;
    try 
    {
        user = new User
        {
             // etc
        };
        entityUser = context.Users.Add(user);
        context.SaveChanges(); // Will throw if the entity already exists
    } 
    catch (DbUpdateException x)
    when (x.InnerException != null && x.InnerException.Message.StartsWith("Cannot insert duplicate key row in object"))
    {
        if (entityUser != null)
        {
            // Detach the entity to stop it hanging around on the context
            entityUser.State = EntityState.Detached;
        }
        var user = context.Users.Find(userId);
        if (user != null) // just in case someone deleted it in the mean time
        {
            // update the user
            user.property = newProperty;
            context.SaveChanges();
        }
    }
}

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