并发 Web api 请求以及如何处理 ASP.NET 核心中的状态

The*_*sen 3 c# concurrency design-patterns asp.net-web-api asp.net-core

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

队列有 10 个可用位置。

如果所有 10 个位置都填满,我们会返回一条消息,说“队列已满”。

如果正好有 3 个用户加入,我们返回 true。

如果加入的用户数不是 3,那么我们返回 false。

200 个对触发满意的用户已准备好加入和离开不同的队列。他们都同时调用“加入”和“离开”端点。

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

一种选择是将添加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;
    }
}
Run Code Online (Sandbox Code Playgroud)

另一种选择是使QueueService瞬态,但随后我失去了服务的状态,并且为每个请求提供了一个新实例,这使得 lock() 毫无意义。

问题:

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

[2] 有没有我错过的精彩模式?或者我怎么能以不同的方式处理这个?

The*_*sen 6

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

创建一个带有 a 的静态(或单例)类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;
    }
Run Code Online (Sandbox Code Playgroud)

_context 不再有问题。

通过这种方式,我可以以良好且受控的方式处理并发请求。

如果在某个时候多个服务器发挥作用,消息代理或 ESB 也可能是一种解决方案。


usr*_*usr 5

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

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

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

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

这在这里似乎完全可能和合适。

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

如果您坚持内存中状态,则拆分单例和瞬态组件。将全局状态放入单例类中,并将作用于该状态的操作放入临时类中。这样,诸如数据库访问之类的瞬态组件的依赖注入非常简单和干净。全局类应该很小(也许只是数据字段)。