К написанию этого поста меня подтолкнуло большое количество гайдов по Entity Framework, в которых задача реализации оптимистического конкурентного доступа (бррр! далее просто optimistic concurrency) сводится к добавлению столбца с типом rowversion (timestamp) в БД, либо использованию атрибута ConcurrencyCheck на интересующих полях класса. Идея такого подхода достаточно проста: выбранные поля будут включены в раздел WHERE инструкции UPDATE. Зачем? Рассмотрим простейший случай:
Получение данных из БД -> Изменение данных -> Сохранение в БД.
Если между загрузкой и сохранением данные в БД были изменены другим потоком/процессом, EF сможет определить конфликт, сравнив версии строк таблицы (если используется rowversion) либо попарно сравнив значения свойств, помеченных ConcurrencyCheck. В результате мы получим исключение, а данные в БД сохранены не будут. Причем, даже объявлять транзакцию в явном виде не нужно (хорошая идея, ага).
Все нижеописанные действия производятся над одной и той же строкой данных стандартными средствами EntityFramework. Сравним поведение EF для случаев:
1) транзакции используют уровень изоляции SNAPSHOT (вариант SQL Server);
Сценарий #1.
В обоих случаях при коммите Транзакции #2 EF выбросит исключение, различается лишь его тип:
SNAPSHOT: DbUpdateException
ROWVERSION: DbUpdateConcurrencyException
ОК, это полностью ожидаемое поведение для оптимистического доступа.
Сценарий #2.
Результаты:
SNAPSHOT: Транзакция #2 без задержек и блокировок считывает неизмененные данные (точнее, их последнюю зафиксированную версию).
ROWVERSION: EntityCommandExecutionException! (по таймауту)
EF выбрасывает исключение, поскольку Транзакция #1 накладывает монопольную блокировку на изменяемую строку данных, а на сервере у нас READ COMMITTED, т.е. на сервере установлен пессимистический конкурентный доступ. В этом случае, Транзакция #2 будет терпеливо ждать снятия монопольной блокировки до истечения определенного периода времени (по-умолчанию 30 с.). Очевидно, что в данном случае мы лишаемся главного преимущества optimistic concurrency - отсутствия блокировок читающих процессов. Поэтому использовать ROWVERSION Без включенной на уровне сервера optimistic concurrency не имеет смысла. Вероятно, поэтому в EF6 CodeFirst уровень изоляции для создаваемой БД по-умолчанию установлен в READ COMMITTED SNAPSHOT. Хороший вопрос - а зачем нам тогда вообще нужен ROWVERSION?
Получение данных из БД -> Изменение данных -> Сохранение в БД.
Если между загрузкой и сохранением данные в БД были изменены другим потоком/процессом, 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?
Комментариев нет:
Отправить комментарий