并发Web API请求以及如何处理ASP.NET Core中的状态

时间:2019-04-10 09:02:59

标签: c# asp.net-web-api asp.net-core design-patterns concurrency

由多个Web api端点组成的ASP.NET core 2.1应用程序(实体框架)。 其中之一是“加入”端点,用户可以在其中加入队列。 另一个是“离开”端点,用户可以在其中离开队列。

队列有10个可用位置。

如果所有10个地点均已填写完毕,我们将向其返回一条消息,指出“队列已满。”

如果刚好有3个用户加入,则返回true。

如果加入的用户数不是3,则返回false。

200个满足触发条件的用户准备加入并离开不同的队列。他们都同时呼叫“ join”和“ leave”端点。

这意味着我们必须按顺序处理传入的请求,以确保以一种良好且可控制的方式将用户添加和删除到正确的队列。 (对吗?)

一种选择是将QueueService类添加为AddSingleton<>中的IServiceCollection,然后创建lock()以确保一次只能输入一个用户。但是,由于dbContext被注册为AddTransient<>AddScoped<>,我们如何处理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

另一种选择是使{{1}}处于瞬态状态,但是随后我失去了服务状态,并且为每个请求提供了一个新实例,从而使lock()毫无意义。

问题:

[1]我应该改为处理内存中队列的状态吗?如果是,如何将其与数据库对齐?

[2]是否有我错过的出色模式?还是我该如何处理?

2 个答案:

答案 0 :(得分:2)

  

这意味着我们必须按顺序处理传入的请求,以确保以一种良好且可控制的方式将用户添加和删除到正确的队列。 (对吗?)

基本上是。这些请求可以并发执行,但是它们必须以某种方式彼此同步(例如,锁或数据库事务)。

  

我应该改为处理内存中队列的状态吗?

通常,最好使Web应用程序无状态且独立于请求。在这种模式下状态存储在数据库中。巨大的优势在于,无需进行同步,并且该应用程序可以在多台服务器上运行。另外,如果应用程序重新启动(或崩溃),则不会丢失任何状态。

在这里看来这完全有可能并且适当。

将状态放入关系数据库中,并使用并发控制使并发访问安全。例如,使用可序列化的隔离级别,并在出现死锁的情况下重试。这为您提供了一个非常不错的编程模型,在该模型中,您假装自己是数据库的唯一用户,但它绝对安全。必须将所有访问权限都放入事务中,如果发生死锁,则必须重试整个事务。

如果您坚持使用内存中状态,则请拆分单例和瞬态组件。将全局状态置于单例类中,并将对该状态进行操作的操作置于瞬时状态中。通过这种方式,临时组件(例如数据库访问)的依赖项注入非常容易且干净。全局类应该很小(也许只是数据字段)。

答案 1 :(得分:0)

我最终得到了这个简单的解决方案。

使用ConcurrentDictionary<int, object>创建一个静态(或单例)类,该类带有queueId和一个锁。

创建新队列后,将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也可以作为解决方案。