Атомное приращение с инфраструктурой сущностей
У меня есть сервер MySQL, к которому я обращаюсь, используя Entity Framework 4.0. В базе данных у меня есть таблица с именем Работы, в которую входят некоторые подсчеты. Я разрабатываю веб-сайт с Asp.net. В этой таблице одновременно появляется еще один пользователь. И эта ситуация вызывает неправильную проблему изъятия.
Мой код:
dbEntities myEntity = new dbEntities();
var currentWork = myEntity.works.Where(xXx => xXx.RID == 208).FirstOrDefault();
Console.WriteLine("Access work");
if (currentWork != null)
{
Console.WriteLine("Access is not null");
currentWork.WordCount += 5;//Default WordCount is 0
Console.WriteLine("Count changed");
myEntity.SaveChanges();
Console.WriteLine("Save changes");
}
Console.WriteLine("Current Count:" + currentWork.WordCount);
Если один поток, кроме потока, обращается к базе данных одновременно, остаются только последние изменения.
Токовый выход:
t1: Thread One - t2: Thread Two
t1: Работа с доступом
t2: Работа с доступом
t2: доступ не равен null
t1: Доступ не равен null
t1: смененный счетчик
t2: количество измененных
t1: сохранить изменения
t2: сохранить изменения
t1: Текущее количество: 5
t2: Текущее количество: 5
Ожидаемый результат:
t1: Работа с доступом
t2: Работа с доступом
t2: доступ не равен null
t1: Доступ не равен null
t1: смененный счетчик
t2: количество измененных
t1: сохранить изменения
t2: сохранить изменения
t1: Текущее количество: 5
t2: Текущее количество: 10
Я знаю, почему эта проблема возникает, потому что этот код не является атомарным. Как я могу запустить атомную операцию?
Ответы
Ответ 1
В Entity Framework вы не можете сделать это "атомной" операцией. У вас есть следующие шаги:
- Загрузить объект из базы данных
- Сменить счетчик в памяти
- Сохранить измененный объект в базе данных
В промежутке между этими шагами другой клиент может загрузить объект из базы данных, которая все еще имеет старое значение.
Лучший способ справиться с этой ситуацией - использовать оптимистичный concurrency. В основном это означает, что изменение на шаге 3 не будет сохранено, если счетчик больше не тот, что был, когда вы загрузили объект на шаге 1. Вместо этого вы получите исключение, которое вы можете обработать, перезагрузив объект и повторное внесение изменений.
Рабочий процесс будет выглядеть следующим образом:
- В объекте
Work
свойство WordCount
должно быть помечено как токен concurrency (аннотации или Fluent API в случае Code-First)
- Загрузить объект из базы данных
- Сменить счетчик в памяти
- Вызвать
SaveChanges
в блоке try-catch
и перехватить исключения типа DbUpdateConcurrencyException
- Если возникает исключение, перезагрузите объект в блоке
catch
из базы данных, снова примените это изменение и снова вызовите SaveChanges
- Повторите последний шаг, пока не произойдет больше исключений
В этом ответе вы можете найти пример кода для этой процедуры (используя DbContext
).
Ответ 2
Следующий способ будет работать, если вы размещаете свой сайт в одном процессе (он не будет работать с веб-фермой или веб-gardsen):
private static readonly Locker = new object();
void Foo()
{
lock(Locker)
{
dbEntities myEntity = new dbEntities();
var currentWork = myEntity.works.Where(xXx => xXx.RID == 208).FirstOrDefault();
Console.WriteLine("Access work");
if (currentWork != null)
{
Console.WriteLine("Access is not null");
currentWork.WordCount += 5;//Default WordCount is 0
Console.WriteLine("Count changed");
myEntity.SaveChanges();
Console.WriteLine("Save changes");
}
Console.WriteLine("Current Count:" + currentWork.WordCount);
}
}
Что еще вы можете сделать, это использовать необработанный SQL-запрос через ObjectContext:
if (currentWork != null)
{
Console.WriteLine("Access is not null");
myEntity.ExecuteStoredCommand("UPDATE works SET WordCount = WordCount + 5 WHERE RID = @rid", new MySqlParameter("@rid", MySqlDbType.Int32){Value = 208)
Console.WriteLine("Count changed");
}
var record = myEntity.works.FirstOrDefault(xXx => xXx.RID == 208);
if(record != null)
Console.WriteLine("Current Count:" + record .WordCount);