Как может быть deadlock для выборки из базы?

157
09 июня 2019, 23:20

Разбираюсь в не своем коде, там много обращений в БД(MS SQL SERVER) с использованием generic репозитория и много очень запутанного кода(поэтому его демонстрации не будет).

Бывает такое что при работе вылетает exception deadlock на методе выборки из базы.

Если упростить описание этого процесса, то для выборки из DbSet<TEntity> применяются данные методы :

IQueryable.OrderByDescending
Queryable.Where   
Queryable.Skip  
QueryableExtensions.Include

До и после этой выборки, но до применения общего context.SaveChanges могут происходить любые Update Insert Delete Select в любых количествах.

Искал как может происходить deadlock и находил только описание случая с 2 транзакциями на Update которые блокируют ресурсы друг друга. Но в случае с Select не понимаю как такое может произойти. Буду рад примеру такой ситуации.

Не очень разбираюсь, но возможно изолированность транзакций как то может влиять на это. Или может это проблемы на стороне c#, а не базы.

System.Data.Entity.Core.EntityCommandExecutionExce
    ption: An error occurred while reading from the store provider's data reader. Se
    e the inner exception for details. ---> System.Data.SqlClient.SqlException: Tran
    saction (Process ID 106) was deadlocked on lock resources with another process a
    nd has been chosen as the deadlock victim. Rerun the transaction.
       at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolea
    n breakConnection, Action`1 wrapCloseInAction)
       at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception
    , Boolean breakConnection, Action`1 wrapCloseInAction)
       at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObj
    ect stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
       at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand
     cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler,
    TdsParserStateObject stateObj, Boolean& dataReady)
       at System.Data.SqlClient.SqlDataReader.TryHasMoreRows(Boolean& moreRows)
       at System.Data.SqlClient.SqlDataReader.TryReadInternal(Boolean setTimeout, Bo
    olean& more)
       at System.Data.SqlClient.SqlDataReader.Read()
       at System.Data.Entity.Core.Common.Internal.Materialization.Shaper`1.StoreRead
    ()
       --- End of inner exception stack trace ---
       at System.Data.Entity.Core.Common.Internal.Materialization.Shaper`1.HandleRea
    derException(Exception e)
       at System.Data.Entity.Core.Common.Internal.Materialization.Shaper`1.StoreRead
    ()
       at System.Data.Entity.Core.Common.Internal.Materialization.Shaper`1.SimpleEnu
    merator.MoveNext()
       at System.Data.Entity.Internal.LazyEnumerator`1.MoveNext()
       at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
       at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
Answer 1

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

Сильно упрощенный пример::

По умолчанию SELECT ставит S-локи. UPDATE - ставит X-локи.
S и X-локи несовместимы - их нельзя одновременно поставить на один и тот же объект. Тот, кто попытается поставить лок вторым, будет ждать, пока первый его снимет.

У вас одновременно выполняются две транзакции:

  1. Транзакция 1 ставит X-лок на объект A (UPDATE A...)
  2. Транзакция 2 ставит X-лок на объект B (UPDATE B...)
  3. Транзакция 1 пытается поставить S-лок на объект B (SELECT FROM B...). Ждет, пока будет снят существующий лок от транзакции 2.
  4. Транзакция 2 пытается поставить S-лок на объект A (SELECT FROM A...). Ждет, пока будет снят существующий лок от транзакции 1.
  5. В (4) SQL Server понимает, что транзакции ждут друг друга, и убивает одну из них как deadlock victim.

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

Стандартный способ избежать дедлоков на чтении - перейти на уровень изоляции транзакций, в котором SELECT не ставит локи. Это или SNAPSHOT, или READ COMMITTED SNAPSHOT.

Эти уровни изоляции транзакций используют row versioning вместо блокировок, и SELECT-ы при них читают копию данных, если к моменту чтения есть незакомитанные изменения другой транзакции.

Разница между ними - в обработке записи. READ COMMITTED SNAPSHOT ведет себя как обычный READ COMMITTED, и просто ставит U/X локи. SNAPSHOT пишет в копию, и падает на коммите транзакции, если кто-то уже успел записать до него.

Если у вас уже используется READ COMMITTED (он почти всегда и везде используется по умолчанию), то достаточно просто включить READ COMMITTED SNAPSHOT на уровне базы, и дедлоки пропадут:

ALTER DATABASE MyDatabase  
SET ALLOW_SNAPSHOT_ISOLATION ON  
ALTER DATABASE MyDatabase  
SET READ_COMMITTED_SNAPSHOT ON  

Отдельно стоит упомянуть работу с транзакциями через TransactionScope. Этот замечательный класс по умолчанию переписывает стандартный для EF и SQL Server уровень READ COMMITTED на SERIALIZABLE, что почти гарантирует дедлоки. Если пользуетесь им - не забывайте вручную выставлять нужный вам уровень изоляции в конструкторе.

READ ALSO
Как вызвать ошибку понятную для COM?

Как вызвать ошибку понятную для COM?

Как реализовать вызов ошибки, как com-объект?

168
Int и bool из С++. Как портировать на C#?

Int и bool из С++. Как портировать на C#?

У меня есть вот такая строчка в С++ коде: sects[low]bound = low

138
Как правильно отправить List&lt;string[]&gt; через json

Как правильно отправить List<string[]> через json

Подскажите пожалуйста! Имеется контроллер на aspnet в котором получаю список из базы данных, далее этот список надо отправить на мобильное приложение

156
Создание отчетов в reportviewer из хранимой процедуры

Создание отчетов в reportviewer из хранимой процедуры

Использую SQL Server 2014, VS 2017 Суть в том, что нужно по параметрам создавать отчеты, например, вывести какие-нибудь записи за указанный период времениЯ...

131