Как заблокировать таблицу при чтении, используя Entity Framework? - программирование
Подтвердить что ты не робот

Как заблокировать таблицу при чтении, используя Entity Framework?

У меня есть SQL Server (2012), к которому я обращаюсь, используя Entity Framework (4.1). В базе данных у меня есть таблица с названием URL, в которой независимый процесс передает новые URL-адреса. Запись в таблице URL может быть в состоянии "Новый", "В процессе" или "Обработан".

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

var newUrl = dbEntity.URLs.FirstOrDefault(url => url.StatusID == (int) URLStatus.New);
if(newUrl != null)
{
    newUrl.StatusID = (int) URLStatus.InProcess;
    dbEntity.SaveChanges();
}
//Process the URL

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

Есть ли способ сделать атомарную последовательность select-then-update атомами, чтобы избежать таких столкновений?

4b9b3361

Ответ 1

Этот ответ не мешает одновременному чтению! Это не правильный ответ на этот вопрос!

Автор здесь: Я был неправ. Я не могу удалить этот ответ, потому что это принятый ответ. @Gilad, если вы не принимаете мой ответ, я удалю его.

Попробуйте следующее:

using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
{
    var newUrl = dbEntity.URLs.FirstOrDefault(url => url.StatusID == (int) URLStatus.New);
    if(newUrl != null)
    {
        newUrl.StatusID = (int) URLStatus.InProcess;
        dbEntity.SaveChanges();
    }
    scope.Complete();
}

IsolationLevel.RepeatableRead применит блокировку ко всем строкам read таким образом, что Thread 2 не может читать из Table A, если Table A был прочитан Thread 1 и Thread 1 не завершите транзакцию. (По комментариям это не так)

Просто обратите внимание на TransactionScopes:

Когда вы окружаете свой код с помощью TransactionScope, вы не создаете никаких транзакций, вы просто создаете область, в которой может потребоваться транзакция. Когда вы читаете данные insinde в своем using, Entity Framework создаст транзакцию, но это больше не будет, потому что теперь из-за TransactionScope вы отвечаете за транзакцию. То есть: вы определяете IsolationLevel, и вы несете ответственность за совершение или откат транзакции. По умолчанию уровень изоляции будет применять блокировку write. То есть, если я напишу URL-адрес, он заблокирует таблицу URL-адресов для чтения и записи. Чтобы применить блокировку при чтении, вы используете IsolationLevel.RepeatableRead.

Если вы создадите больше транзакций внутри своего TransactionScope, оно будет распространено на распределенную транзакцию, но это выходит за рамки моего ответа.

Ответ 2

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

using (Entities entities = new Entities())
using (TransactionScope scope = new TransactionScope())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Complete();
}

Обновление. В Entity Framework 6, особенно с кодом async/await, вам нужно обрабатывать транзакции по-разному. Это было сбой для нас после некоторых преобразований.

using (Entities entities = new Entities())
using (DbContextTransaction scope = entities.Database.BeginTransaction())
{
    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Commit();
}

Ответ 3

Я не могу добавить комментарий к Андре, но я обеспокоен этим комментарием "IsolationLevel.RepeatableRead применит блокировку ко всем строкам, которые читаются таким образом, что Thread 2 не может прочитать из таблицы A, если таблица A была прочитана в Thread 1, а Thread 1 не завершил транзакцию".

Повторяемое чтение только говорит, что вы будете удерживать все блокировки до конца транзакции. Когда вы используете этот уровень изоляции в транзакции и читаете строку (скажем, максимальное значение), "Shared" блокировка выдается и будет удерживаться до завершения транзакции. Эта совместная блокировка не позволит другому потоку обновлять строку (обновление попытается применить исключающую блокировку в строке и заблокировать существующей общей блокировкой), но это позволит другому потоку читать значение (второй поток добавит еще одну общую блокировку в строке - что разрешено (поэтому они называются совместно используемыми блокировками)). Поэтому, чтобы сделать приведенное выше утверждение правильным, нужно сказать: "The IsolationLevel.RepeatableRead применит блокировку ко всем строкам, которые читаются таким образом, что Thread 2 не может обновить таблицу A, если таблица A был прочитан нитью 1, и нить 1 не завершила транзакцию".

Для исходного вопроса вам нужно будет использовать уровень изоляции повторяемого чтения И увеличьте блокировку до блокировки Exclusive, чтобы предотвратить чтение и обновление двух процессов. Все решения включали бы отображение EF в пользовательский SQL (поскольку эскалация типа блокировки не встроена в EF). Вы можете использовать ответ jocull, или вы можете использовать обновление с предложением вывода для блокировки строк (операторы обновления всегда получают блокировку Exclusive и в 2008 году или выше могут возвращать набор результатов).

Ответ 4

Ответ, предоставленный @jocull, велик. Я предлагаю эту настройку:

Вместо этого:

"SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)"

Сделайте это:

"SELECT TOP 0 NULL FROM MyTable WITH (TABLOCKX)"

Это более общий. Вы можете создать вспомогательный метод, который просто принимает имя таблицы в качестве параметра. Не нужно знать данные (иначе имена столбцов), и нет необходимости фактически извлекать запись по трубе (aka TOP 1)

Ответ 5

Вы можете попробовать передать подсказку UPDLOCK в базу данных. Таким образом, то, что он выбирает, также включает эксклюзивную блокировку, чтобы сохранить изменения (а не просто блокировку чтения, которую он позже пытается обновить при сохранении). Holdlock, предложенный jocull выше, также является хорошей идеей.

private static TestEntity GetFirstEntity(Context context) {
return context.TestEntities
          .SqlQuery("SELECT TOP 1 Id, Value FROM TestEntities WITH (UPDLOCK)")
          .Single();
}

Я настоятельно советую рассмотреть оптимистичный параллелизм: https://www.entityframeworktutorial.net/EntityFramework5/handle-concurrency-in-entity-framework.aspx