Предотвращение состояния гонки if-существующие-update-else-insert в Entity Framework - PullRequest
27 голосов
/ 30 мая 2011

Я читал другие вопросы о том, как реализовать семантику if-существующие-insert-else-update в EF, но либо я не понимаю, как работают ответы, либо они фактически не решают проблему. Обычное предлагаемое решение - заключить работу в область транзакции (например: Реализация вставки «если не существует» с использованием 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();
    }
}

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

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

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

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

Есть идеи?

РЕДАКТИРОВАТЬ : я пытался выполнить вышеупомянутый код одновременно в двух разных потоках, используя один и тот же идентификатор пользователя, и, несмотря на выполнение сериализуемых транзакций, они оба могли одновременно входить в критическую секцию (*). Это приводит к возникновению исключения UpdateException, когда второй поток пытается вставить тот же идентификатор пользователя, который только что был добавлен первым. Это связано с тем, что, как указал Ладислав ниже, сериализуемая транзакция получает эксклюзивные блокировки только после того, как она начала изменять данные, а не читать.

Ответы [ 4 ]

12 голосов
/ 30 мая 2011

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

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

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

1 голос
/ 18 января 2018

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

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

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();
        }
    }
}

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

0 голосов
/ 07 июня 2011

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

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

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

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

0 голосов
/ 30 мая 2011

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

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

...