-->

суббота, 8 августа 2015 г.

Optimistic concurrency в EF + SQL Server

К написанию этого поста меня подтолкнуло большое количество гайдов по Entity Framework, в которых задача реализации оптимистического конкурентного доступа (бррр! далее просто optimistic concurrency) сводится к добавлению столбца с типом rowversion (timestamp) в БД, либо использованию атрибута ConcurrencyCheck на интересующих полях класса. Идея такого подхода достаточно проста: выбранные поля будут включены в раздел WHERE инструкции UPDATE. Зачем? Рассмотрим простейший случай:

Получение данных из БД -> Изменение данных -> Сохранение в БД.

Если между загрузкой и сохранением данные в БД были изменены другим потоком/процессом, EF сможет определить конфликт, сравнив версии строк таблицы (если используется rowversion) либо попарно сравнив значения свойств, помеченных ConcurrencyCheck. В результате мы получим исключение, а данные в БД сохранены не будут. Причем, даже объявлять транзакцию в явном виде не нужно (хорошая идея, ага).

Это здорово - иметь optimistic concurrency из коробки, и формально не зависящее от БД. Но будем откровенны: наиболее популярное СУБД в одной упряжке с EF - это SQL Server, а процент проектов, где смена СУБД допускается хотя бы в перспективе - исчезающе мал. Так почему бы не воспользоваться средствами SQL Server? Конечно же, я говорю об уровнях изоляции SNAPSHOT и READ COMMITTED SNAPSHOT. У меня бы не получилось рассказать об изоляции транзакций лучше, чем у авторов моей любимой Microsoft SQL Server 2012 Internals, поэтому не буду и пытаться. Попробуем сравнить подходы EF и SQL Server к реализации optimistic concurrency.


Все нижеописанные действия производятся над одной и той же строкой данных стандартными средствами EntityFramework. Сравним поведение EF для случаев:
1) транзакции используют уровень изоляции SNAPSHOT (вариант SQL Server);

using (var context = new DbConnection())
{
 using (var transaction = context.Database.BeginTransaction(IsolationLevel.Snapshot))
 {
  var entity = context.Entities.Find(id);
  //update entity
  context.SaveChanges();

  transaction.Commit();
 }
}
2) entity имеет столбец ROWVERSION, для которого в модели установлено свойство ConcurrencyMode = Fixed (вариант EF). Транзакция не объявляется, т.е. используется по-умолчанию установленный в SQL Server уровень изоляции READ COMMITTED:

using(context = new DataContext())
{
 var entity = context.Entities.Find(id);
 //update entity
 context.SaveChanges();
}

Сценарий #1.
Tran#1 Tran#2
BEGIN TRAN SELECT
BEGIN TRAN SELECT
UPDATE COMMIT TRAN
UPDATE COMMIT TRAN

В обоих случаях при коммите Транзакции #2 EF выбросит исключение, различается лишь его тип:
SNAPSHOT: DbUpdateException
ROWVERSION: DbUpdateConcurrencyException
ОК, это полностью ожидаемое поведение для оптимистического доступа.

Сценарий #2.
Tran#1 Tran#2
BEGIN TRAN UPDATE
BEGIN TRAN SELECT
Результаты:
SNAPSHOT: Транзакция #2 без задержек и блокировок считывает неизмененные данные (точнее, их последнюю зафиксированную версию).
ROWVERSION: EntityCommandExecutionException! (по таймауту)

EF выбрасывает исключение, поскольку Транзакция #1 накладывает монопольную блокировку на изменяемую строку данных, а на сервере у нас READ COMMITTED, т.е. на сервере установлен пессимистический конкурентный доступ. В этом случае, Транзакция #2 будет терпеливо ждать снятия монопольной блокировки до истечения определенного периода времени (по-умолчанию 30 с.). Очевидно, что в данном случае мы лишаемся главного преимущества optimistic concurrency - отсутствия блокировок читающих процессов. Поэтому использовать ROWVERSION Без включенной на уровне сервера optimistic concurrency не имеет смысла. Вероятно, поэтому в EF6 CodeFirst уровень изоляции для создаваемой БД по-умолчанию установлен в READ COMMITTED SNAPSHOT. Хороший вопрос - а зачем нам тогда вообще нужен ROWVERSION?

Комментариев нет:

Отправить комментарий