Корректная обработка параллелизма с использованием EF Core 2.1 с SQL Server - PullRequest
0 голосов
/ 03 декабря 2018

В настоящее время я работаю над 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, большая часть кода, который я видел, не использует блокировку.

, что приводит меня к моему вопросу:это правильный способ сделать это?

Ура и заранее спасибо.

Ответы [ 2 ]

0 голосов
/ 10 декабря 2018

Я предлагаю вам поймать DbUpdateConcurrencyException и использовать entry.GetDatabaseValues(); и entry.OriginalValues.SetValues(databaseValues); в вашей логике повторения.Нет необходимости блокировать БД.

Вот пример на EF Core документации страница:

using (var context = new PersonContext())
{
    // Fetch a person from database and change phone number
    var person = context.People.Single(p => p.PersonId == 1);
    person.PhoneNumber = "555-555-5555";

    // Change the person's name in the database to simulate a concurrency conflict
    context.Database.ExecuteSqlCommand(
        "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

    var saved = false;
    while (!saved)
    {
        try
        {
            // Attempt to save changes to the database
            context.SaveChanges();
            saved = true;
        }
        catch (DbUpdateConcurrencyException ex)
        {
            foreach (var entry in ex.Entries)
            {
                if (entry.Entity is Person)
                {
                    var proposedValues = entry.CurrentValues;
                    var databaseValues = entry.GetDatabaseValues();

                    foreach (var property in proposedValues.Properties)
                    {
                        var proposedValue = proposedValues[property];
                        var databaseValue = databaseValues[property];

                        // TODO: decide which value should be written to database
                        // proposedValues[property] = <value to be saved>;
                    }

                    // Refresh original values to bypass next concurrency check
                    entry.OriginalValues.SetValues(databaseValues);
                }
                else
                {
                    throw new NotSupportedException(
                        "Don't know how to handle concurrency conflicts for "
                        + entry.Metadata.Name);
                }
            }
        }
    }
}
0 голосов
/ 06 декабря 2018

Почему вы не решаете проблему параллелизма в коде, почему она должна быть на уровне БД?

У вас может быть метод, который обновляет значение данного кошелька с заданным значением, и вы можетеиспользуйте простой замок там.Например:

private readonly object walletLock = new object();

public void UpdateWalletAmount(int userId, int amount)
{
    lock (balanceLock)
    {
         Wallet w = _context.Wallets.Where(ww => ww.UserId == userId).FirstOrDefault();
         w.Amount = w.Amount + amount;
         w.Inserts++;
         _context.Wallets.Update(w);
         _context.SaveChanges();
    }
}

Таким образом, ваши методы будут выглядеть следующим образом:

[HttpGet]
[Route("request1")]
public async Task<string> request1()
{
    try
    {
        UpdateWalletAmount(1, 10);
    }
    catch (Exception ex)
    {
        // log error
    }
    return "request 1 executed";
}

[HttpGet]
[Route("request2")]
public async Task<string> request2()
{
    try
    {
        UpdateWalletAmount(1, 20);
    }
    catch (Exception ex)
    {
        // log error
    }
    return "request 2 executed";
}

Вам даже не нужно использовать транзакцию в этом контексте.

...