У меня есть такой код модуля статистики:
using(TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions {IsolationLevel = IsolationLevel.RepeatableRead}))
{
PersonDayStatisticsUnit pdsu = db.PersonDayStatisticsUnits.FirstOrDefault(x => x.UnitId == su.Id && x.Date == now && x.PersonAnonimousGuid == personGuid);
if (pdsu == null)
{
pdsu = new PersonDayStatisticsUnit()
{
UnitId = su.Id,
Count = 1,
Date = now,
PersonAnonimousGuid = personGuid
};
db.PersonDayStatisticsUnits.Add(pdsu);
ret = true;
}
else
{
pdsu.Count++;
ret = false;
}
db.SaveChanges();
scope.Complete();
}
Этот код работает в ASP.NET MVC 5 контексте.
Обнаружил что этот код падает, с ошибкой, что не может создать запись с дублирующимися ключами.
В чём ошибка я понял, что видимо два потока зашли в транзакцию и попытались создать одинаковый объект PersonDayStatisticsUnit, db.PersonDayStatisticsUnits.Add(pdsu).
То есть что-то я недодумал в коде...
Как я понимаю весь этот код можно просто поместить в lock, это должно решить проблему.
Нужен ли здесь lock и какое наилучшее решение ситуации? Как вообще соотносятся транзакции с lock-ами, можно или нужно их делать одновременно, или же тут надо просто сделать другой вид транзакции IsolationLevel?
Ни выставление IsolationLevel, ни lock (в общем случае) тут не помогут.
lock поможет в случае, если у вас ровно один экземпляр приложения. Т.е. это хорошее решение, если ваше приложение - небольшое, и его не планируется масштабировать.
В общем случае - если у вас два или более серверов - лок не поможет.
IsolationLevel не поможет потому, что он работает не так, как вы ожидаете. Он контролирует две вещи:
Если при попытке поставить блокировку на ресурсе (таблице, строке, разделе) стоит несовместимая блокировка - запрос ждет, пока именно эта блокировка будет снята. Например, можно поставить два S лока одновременно, S и U одновременно, но нельзя поставить два U.
Это достаточно подробно и обширно расписано в Transaction Locking and Row Versioning Guide, но если коротко, то
Так что при любом уровне изоляции транзакций вы можете делать одновременные выборки.
Если вам нужно запретить одновременные выборки - вам нужно - ставить при выборке не S-lock, а U-lock или X-lock - удерживать его до конца транзакции
Тип и уровень блокировки можно поменять подсказкой в тексте запроса (TABLE HINT)
Т.е. вам или в первом SELECT, или перед ним нужно выполнить SQL вида
SELECT top 1 somecolumn FROM PersonDayStatisticsUnits
WHERE (xlock, tablock)
Это заблокирует выполнение точно такого же SQL в другой транзации. В качестве альтернативы - можно поискать что-то готовое для EF, например HintsInterceptor, и навесить соответствующие хинты на первый запрос или на таблицу целиком.
Я бы в таком случае попробовал установить IsolationLevel Serializable. Если не поможет, тогда вероятно нужен lock.
Основные этапы разработки сайта для стоматологической клиники
Продвижение своими сайтами как стратегия роста и независимости