Параллельные запросы веб-API и как обрабатывать состояние в ядре ASP.NET - PullRequest
1 голос
/ 10 апреля 2019

Приложение ASP.NET core 2.1 (Entity Framework), состоящее из нескольких конечных точек веб-API. Одной из них является конечная точка «соединения», где пользователи могут присоединиться к очереди. Еще одна конечная точка «выхода», где пользователи могут покинуть очередь.

В очереди 10 доступных мест.

Если все 10 мест заполнены, мы возвращаем сообщение «Очередь заполнена».

Если ровно 3 пользователя присоединились, мы возвращаем true.

Если количество присоединившихся пользователей НЕ равно 3, мы возвращаем false.

200 счастливых пользователей триггера готовы присоединиться и покинуть разные очереди. Все они одновременно вызывают конечную точку "соединения" и "выхода".

Это означает, что мы должны обрабатывать поступающие запросы последовательным образом, чтобы обеспечить возможность добавления и удаления пользователей в правильные очереди удобным и контролируемым образом. (Правильно?)

Один из вариантов - добавить класс QueueService как AddSingleton<> в IServiceCollection, а затем сделать lock(), чтобы обеспечить доступ только одному пользователю за раз. Однако как мы можем обрабатывать dbContext, поскольку он зарегистрирован как AddTransient<> или AddScoped<>?

Psedudo код для присоединяемой части:

public class QueueService
{
    private readonly object _myLock = new object();
    private readonly QueueContext _context;

    public QueueService(QueueContext context)
    {
        _context = context;
    }

    public bool Join(int queueId, int userId)
    {
        lock (_myLock)
        {
            var numberOfUsersInQueue = _context.GetNumberOfUsersInQueue(queueId); <- Problem. 
            if (numberOfUsersInQueue >= 10)
            {
                throw new Exception("Queue is full.");
            }
            else
            {
                _context.AddUserToQueue(queueId, userId); <- Problem.
            }

            numberOfUsersInQueue = _context.GetNumberOfUsersInQueue(queueId); <- Problem.

            if (numberOfUsersInQueue == 3)
            {
                return true;
            }
        }
        return false;
    }
}

Другой вариант - сделать QueueService кратковременным, но тогда я потеряю состояние службы, и для каждого запроса предоставляется новый экземпляр, что делает lock () бессмысленным.

Вопросы:

[1] Должен ли я вместо этого обрабатывать состояние очередей в памяти? Если да, как выровнять его с базой данных?

[2] Есть ли блестящий образец, который я пропустил? Или как я могу справиться с этим по-другому?

Ответы [ 2 ]

2 голосов
/ 10 апреля 2019

Это означает, что мы должны обрабатывать поступающие запросы последовательным способом, чтобы обеспечить возможность добавления и удаления пользователей в правильные очереди удобным и контролируемым образом.(верно?)

По существу, да.Запросы могут выполняться одновременно, но они должны синхронизироваться друг с другом каким-то образом (например, блокировка или транзакции базы данных).

Стоит ли вместо этого обрабатывать состояние очередей в памяти?

Зачастую лучше всего сделать веб-приложение без состояний и запросов независимым.Под этой моделью состояние хранится в базе данных.Огромное преимущество заключается в том, что синхронизация не требуется, и приложение может работать на нескольких серверах.Кроме того, если приложение перезапускается (или аварийно завершает работу), никакое состояние не теряется.

Это кажется вполне возможным и уместным здесь.

Поместите состояние в реляционную базу данных и используйте управление параллелизмом для одновременного доступа.безопасный.Например, используйте сериализуемый уровень изоляции и повторите попытку в случае тупика.Это дает вам очень хорошую модель программирования, в которой вы притворяетесь, что вы единственный пользователь базы данных, но при этом она совершенно безопасна.Весь доступ должен быть помещен в транзакцию, и в случае взаимной блокировки вся транзакция должна быть повторена.

Если вы настаиваете на состоянии в памяти, то разделите одноэлементный и переходный компоненты.Поместите глобальное состояние в одноэлементный класс и поместите операции, действующие на это состояние, во временный.Таким образом, внедрение зависимых компонентов, таких как доступ к базе данных, очень просто и чисто.Глобальный класс должен быть крошечным (может быть, просто полями данных).

0 голосов
/ 11 апреля 2019

Я остановился на этом простом решении.

Создайте статический (или одноэлементный) класс с ConcurrentDictionary<int, object>, который принимает идентификатор очереди и блокировку.

При создании новой очередидобавьте queueId и новый объект блокировки в словарь.

Сделайте класс QueueService AddTransient<>, а затем:

public bool Join(int queueId, int userId)
    {
        var someLock = ConcurrentQueuePlaceHolder.Queues[queueId];
        lock (someLock)
        {
            var numberOfUsersInQueue = _context.GetNumberOfUsersInQueue(queueId); <- Working
            if (numberOfUsersInQueue >= 10)
            {
                throw new Exception("Queue is full.");
            }
            else
            {
                _context.AddUserToQueue(queueId, userId); <- Working
            }

            numberOfUsersInQueue = _context.GetNumberOfUsersInQueue(queueId); <- Working

            if (numberOfUsersInQueue == 3)
            {
                return true;
            }
        }
        return false;
    }

Больше никаких проблем с _context.

Таким образом, я могу обрабатывать параллельные запросы приятным и контролируемым образом.

Если в какой-то момент в игру вступят несколько серверов, брокером сообщений или ESB также может быть решение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...