Web Api - 每个用户互斥量

Web Api - Mutex Per User

我有一个 asp.net 核心 Web Api 应用程序。 在我的应用程序中,我有 Web Api 方法,我想防止来自同一用户的多个请求同时输入。我不介意不同用户请求同时执行。

我不确定如何创建锁以及将它放在哪里。我考虑过创建某种字典,其中包含用户 ID 并对项目执行锁定,但我认为我做对了。另外,如果有多个服务器,并且有一个负载均衡器,会发生什么情况?

示例: 假设每个注册用户每月可以完成 10 个长任务。我需要检查每个用户是否超过了每月限额。如果用户将同时向服务器发送许多请求,他可能被允许执行 10 个以上的操作。我知道我需要锁定该方法,但我确实希望允许其他用户同时执行此操作。

您所要求的根本不是互联网的运作方式。 HTTP 和底层 IP 协议是无状态的,这意味着每个请求都应该 运行 独立于之前发生的事情的任何知识(或同时发生,视情况而定)。如果您担心负载过大,最好的办法是实施与身份验证相关的速率 limiting/throttling。这样,一旦用户完成分配给他们的请求,他们就会被切断。这将产生一个自然的副作用,使针对您的 API 进行编程的开发人员在发送过多请求时更加谨慎。

在这里,为了更彻底一点,您建议的方法的主要问题是我知道它无法实际实施。您可以使用 SemaphoreSlim 之类的东西来创建锁,但它必须是静态的,以便每个请求都使用相同的实例。静态会限制您使用它们的字典的能力,而这正是您需要的。我想这在技术上是可以完成的,但是您必须使用 ConcurrentDictionary,即使那样,也不能保证单线程添加。因此,对同一用户的并发请求可以将并发信号量加载到其中,这违背了整个观点。我想您可以从一开始就为每个用户预先加载带有信号量的字典,但这可能会造成巨大的资源浪费,具体取决于您的用户群。总而言之,当你找到一个如此困难的解决方案时,这是一个好兆头,这是一个好兆头,你可能正在尝试做一些你不应该做的事情。

编辑

阅读您的示例后,我认为这实际上归结为尝试处理请求管道中的工作的问题。当有一些长期 运行ning 任务要完成或只是一些繁重的工作要完成时,第一步应该始终是将其传递给后台服务。这使您可以 return 快速响应。 Web 服务器用于处理请求的线程数量有限,您希望尽快为请求提供服务并return 响应,以免耗尽您的线程池。

您可以使用像 Hangfire 这样的库来处理您的后台工作,或者您可以实现一个 IHostedService as described here 来对工作进行排队。准备好后台服务后,只要收到对此端点的请求,您就可以立即切换到该端点,并且 return 202 Accepted 响应和 URL 客户端可以点击查看状态。这解决了您的直接问题,即不想允许对这个长期 运行ning 作业的大量请求来降低您的 API。它现在基本上只是告诉其他事情去做,然后立即 returning 。

对于您要排队的实际后台工作,您可以在那里检查用户的限额,如果他们超过 10 个请求(您的速率限制),您将立即使工作失败,而无需执行任何操作。如果没有,那你就可以真正开始工作了。

如果愿意,您还可以启用 webhook 支持以在作业完成时通知客户端。您只需允许客户端设置一个您应该在完成时通知的回调 URL,然后当您完成后台任务中的工作时,您会点击该回调。由客户端来处理事情,以决定回调时会发生什么。例如,他们可能决定使用 SignalR 向他们自己的 users/clients.

发送消息

编辑 #2

其实我对此有点好奇。虽然我仍然认为您最好将工作卸载到后台进程,但我能够使用 SemaphoreSlim 创建一个解决方案。本质上,您只是通过信号量对每个请求进行门控,您将在其中检查当前用户的剩余请求。这确实意味着其他用户必须等待此检查完成,然后您可以释放信号量并实际完成工作。这样,至少,在实际的 运行ning 工作期间,您不会阻止其他用户。

首先,将一个字段添加到任何 class 您正在执行此操作的地方:

private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

然后,在实际调用的方法中:

await _semaphore.WaitAsync();

// get remaining requests for user

if (remaining > 0)
{
    // decrement remaining requests for user (this must be done before this next line)

    _semaphore.Release();

    // now do the work
}
else
{
    _semaphore.Release();

    // handle user out of requests (return error, etc.)
}

这本质上是一个瓶颈。要进行适当的检查和递减,一次只能有一个线程通过信号量。这意味着如果您的 API 受到猛烈攻击,请求将排队并且可能需要一段时间才能完成。但是,由于这可能只是一个 SELECT 查询后跟一个 UPDATE 查询,因此释放信号量应该不会花那么长时间。不过,如果你要走这条路,你绝对应该做一些负载测试并观察它。