В настоящее время я работаю над API, использующим ASP.NET Core Web API вместе с Entity Framework Core 2.1 и базой данных SQL Server.API используется для перевода денег с двух счетов A и B. Учитывая характер счета B, который является счетом, принимающим платежи, в одно и то же время может выполняться много одновременных запросов.Как вы знаете, если он не очень хорошо управляется, это может привести к тому, что некоторые пользователи не увидят, как поступают их платежи.
Потратив несколько дней, пытаясь достичь параллелизма, я не могу понять, каков наилучший подход.Для простоты я создал тестовый проект, пытающийся воспроизвести эту проблему параллелизма.
В тестовом проекте у меня есть два маршрута: request1 и request2, каждый из которых выполняет передачу одному и тому же пользователю, первый из которых имеетсумма 10, а вторая - 20. Я положил Thread.sleep(10000)
на первый следующим образом:
[HttpGet]
[Route("request1")]
public async Task<string> request1()
{
using (var transaction = _context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
{
try
{
Wallet w = _context.Wallets.Where(ww => ww.UserId == 1).FirstOrDefault();
Thread.Sleep(10000);
w.Amount = w.Amount + 10;
w.Inserts++;
_context.Wallets.Update(w);
_context.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
}
}
return "request 1 executed";
}
[HttpGet]
[Route("request2")]
public async Task<string> request2()
{
using (var transaction = _context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
{
try
{
Wallet w = _context.Wallets.Where(ww => ww.UserId == 1).FirstOrDefault();
w.Amount = w.Amount + 20;
w.Inserts++;
_context.Wallets.Update(w);
_context.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
}
}
return "request 2 executed";
}
После выполнения request1 и request2 после в браузере, первая транзакция откатывается из-за:
InvalidOperationException: An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure()' to the 'UseSqlServer' call.
Я также могу повторить транзакцию, но разве нет лучшего способа?использование блокировок?
Сериализуемый, будучи самым изолированным уровнем и самым дорогостоящим, как сказано в документации:
Никакие другие транзакции не могут изменять данные, прочитанные текущимтранзакция, пока текущая транзакция не завершится.
Это означает, что никакая другая транзакция не может обновить данные, которые были прочитаны другой транзакцией, которая работает здесь, как предполагалось, так как обновление в маршруте request2 ожидает первую транзакцию(request1) для фиксации.
Проблема здесь в том, что нам нужно заблокировать чтение другими транзакциями после того, как текущая транзакция прочитала строку кошелька, чтобы решить проблему, мне нужно использовать блокировку, чтобы при первом утверждении selectв request1 выполняется, все транзакции после должны ждать окончания 1-ой транзакции, чтобы они могли выбрать правильное значение.Поскольку EF Core не поддерживает блокировку, мне нужно выполнить SQL-запрос напрямую, поэтому при выборе кошелька я добавлю блокировку строки к текущей выбранной строке
//this locks the wallet row with id 1
//and also the default transaction isolation level is enough
Wallet w = _context.Wallets.FromSql("select * from wallets with (XLOCK, ROWLOCK) where id = 1").FirstOrDefault();
Thread.Sleep(10000);
w.Amount = w.Amount + 10;
w.Inserts++;
_context.Wallets.Update(w);
_context.SaveChanges();
transaction.Commit();
Теперь это работает отлично даже после выполнениямножественный запрос, результат перечислений все вместе, правильноВ дополнение к этому я использую таблицу транзакций, в которой хранятся все денежные переводы, сделанные со статусом, чтобы вести учет каждой транзакции в случае, если что-то пошло не так, я могу рассчитать сумму всех кошельков, используя эту таблицу.
Теперь естьдругие способы сделать это, например:
- Хранимая процедура: но я хочу, чтобы моя логика находилась на уровне приложения
- Создание синхронизированного метода для обработки логики базы данных: таким образом, всезапросы к базе данных выполняются в одном потоке, я прочитал пост в блоге, в котором советуется использовать этот подход, но, возможно, мы будем использовать несколько серверов для масштабируемости
Я не знаю, не ищу ли яхорошо, но я не могу найти хороший материал для обработки пессимистического параллелизма с Entity Framework Core, даже когда просматривал Github, большая часть кода, который я видел, не использует блокировку.
, что приводит меня к моему вопросу:это правильный способ сделать это?
Ура и заранее спасибо.