使用等待列表实现RestAPI
我们有一个 rest API,其中一些函数需要几秒钟才能运行。为了能够正常工作,这些函数必须锁定一些文件资源,因此当文件被锁定时,对该函数的任何其他调用都会导致异常。我们很容易简单地检查资源是否可用,一旦可用,就获取它,执行任务,然后释放它。然而,当这样做时,没有考虑谁先调用函数,所以在最坏的情况下,第一个等待的人可能会等待很长时间才能访问它,这可能会导致很长的等待时间。
相反,我想做的是使用某种优先级或等待列表,其中第一个排队的人永远是第一个,有点像那些等待买票的等候室。我尝试在网上查看,但要么不可能,要么找不到合适的词来查找它,所以我想知道它是否有任何实现。然而,英语不是我的母语,所以我不知道我是否在看正确的东西。
功能的实际运行时间并不长,而且一次超过 3 个人实际上需要它会令人惊讶,所以我怀疑延迟会是一个问题,至少在大多数情况下,但重要的是它们不是同时运行。最后,如果不可能,我可能最终会检查文件是否正在使用,如果不是,则等待,但我发现这个解决方案并不“优雅”,而是希望以最好的方式进行。
谢谢
回答
您可能找不到针对此的特定 Code 解决方案,因为其实施将高度特定于您的业务需求、技能、预算和运营环境。
您正在寻找的优雅解决方案可能花费更多或花费更长的时间来实现,在这种情况下,只会使过程失败并强制调用者等待并重试,这可能就是您现在所处的位置。
有不同的架构可用于解决此类问题,但您可以推出自己的架构,让我们分解一下,以便您研究一些更简单的概念:
- 一次一个调用者访问基于 REST 的资源
- FIFO队列以识别呼叫者及其优先级
- 异步响应基于 REST 的资源的调用者的机制
由于这是一个API,我们可以假设一个关键的重要事实,即使这对您现在不是特别适用,您也应该设计您的API,就好像它会那样,否则您会遇到问题:
Web API将部署到冗余或高度可用的基础架构。
想想云、服务器群和负载平衡……但即使您部署到运行apache或IIS的单个服务器,您的 API 通常也会作为多个进程运行。
-
这意味着你不能使用简单的.NET线程的概念,如
lock,Mutex,Semaphore,async,await管理单个用户的访问。 -
这也意味着您不能使用.NET 队列来管理优先级或System.Collections.Concurrent库中的任何内容,因为它不会跨进程或服务器拥有任何知识或访问权限。
-
这也意味着如果您正在访问物理文件,您可能无法依赖传统的文件系统锁,除非文件是从基于云的或以其他方式管理的分布式文件系统提供的。
-
你应该不要试图实现这个作为一个同步是主叫等待来自网络服务器的响应的解决方案。可以做到,但是涉及太多变量,您无法控制以使其可靠,例如调用者在将其检测为超时之前将等待多长时间。
的过程应该是异步
我并不是说您应该使用async await代码模式,而是应该设计流程,以便调用Begin the task,它应该在queue 中注册您的位置,然后有一种机制供调用者检查随着时间的推移任务完成,或回调到原始进程的方式。
在基于 Web 的 API 中,对开始此关键任务的调用应立即或同步返回给调用者,调用者应轮询或等待带有响应的回调。
设计中,如果主叫方完成过程调查是简单的从API的角度实现,因为它的动作大部分的决策逻辑上的下一步该怎么做,出了API的。
-
回调可以采用回发到调用者提供的特定 URL 的形式,也很容易实现,这是许多信用卡支付网关支持的标准模式。如果调用过程支持WebHooks,那么这是相同概念的实现。
-
Web Sockets是一种可用于直接响应调用过程的机制,在 .NET 中,您可以查看SignalR,但您可以从第一个主体开始。
许多解决像您这样的问题的现代 3rd 方产品或服务都基于套接字,因为当您想要实现(接近)实时响应时,它是一种比客户端轮询状态更新更有效的模式。
-
推送通知是另一种您可能熟悉的类似于 WebSockets(通知链的一部分确实使用套接字)的实现,您可以使用这些通知调用客户端应用程序,即使应用程序已在客户端设备上关闭...
- 好吧,不管它在这种情况下的有效性如何,它仍然是一种选择,而且通常很容易实现。
在深入研究回调和套接字之前,请考虑是否在应用程序数据库的记录中设置一个标志,使文件可供下一个调用者使用,或者发送简单的电子邮件、SMS 或推送通知是否就足够了。
的队列需要是外部
对于许多任务,对Begin请求的响应(锁定获取)可能就足够了,仅说明用户 x 正在使用该文件,您应该稍后再试。如果预期的等待时间是几秒或更短,那么客户端进程甚至可能不会向用户显示通知,它可能只是继续重试。
由于上述架构的限制,即使在这种简单的方案是很重要的是,锁定机制是真理的单点存储出方的你执行代码。
它可以是存储在中央位置的简单配置或文本文件,也可以是数据库中的一个条目,也可以是任何其他类型的分布式缓存,但它必须在您的代码进程之外进行管理,以便您的多个实例API 都可以访问它。
您假设我们必须依靠其他人已经为我们解决了这个难题,这是正确的......
我想做的是使用某种优先级或等待名单,其中第一个排队的人永远是第一个,有点像那些等待买票的等候室
您正在描述 FIFO(先进先出)队列。有一些产品存在简化这一点,但最关键的是你的队列机制需要真理的单点存储出方的你执行代码。在这个队列中,你应该存储关于请求的信息(或指向它的指针),以及当任务完成时如何响应。
有什么选择?
无论您选择自己推出,还是使用 3rd 方产品或服务,我仍然鼓励您保留自己的任务状态记录。如果您有数据库后端,那么这可能涉及一个新表来记录以下信息:
- 谁请求了任务
- 要求什么任务
- 任务是什么时候请求的
- 任务对什么数据进行操作
- 任务处于什么状态
- 任务完成后做什么,或通知谁
每次调用Begin the task 都应该在这个表中产生一个新记录,如果没有别的,它可以只是执行日志,但最终它可以形成对请求的响应的基础。
响应应包括一些标识符或接收编号,可用于查找此排队的任务请求条目。
使用数据库的好处是我们也可以用它来实现一个简单的队列。不好的是,这会变得非常低效,无论如何让我们看看它,因为它很容易上手。
如果您的请求模式在某些情况下“可能”能够同步运行而在其他情况下必须排队,则您应该考虑将其设置为ALWAYS queued。
如果我们强制模式始终是异步的,您可以同时简化 API 代码和客户端代码。它将减少处理这两种情况所需的分支逻辑,如果队列为空,我们可以开始手动执行。
一旦您有请求进入代表临时队列的表,您现在可以编写一个单独的进程来编组任务,以确保一次执行一次,从数据库的角度来看,这可能是一个简单的伪查询:
select the first Task
where the task is NOT in progress
select the oldest queued request
for that task
where the task has not yet been completed
从数据库的角度来看,您将定期执行此查询,以检测要处理的下一行。
然后您需要另一个进程来执行该任务,这可以作为 API 中的另一个端点编写,但它应该能够与原始请求处理程序完全隔离地调用。将其编写为独立的控制台应用程序可能更容易,或者您将编组或协调逻辑写入控制台应用程序,并让它在检测到要完成的新任务时调用正确的端点。
在任务开始时,代码应该更新队列表以指示任务正在处理中。在任务结束时,代码还应更新队列表以指示任务已完成。
您不能依赖您的代码在数据库完成时或完成后始终更新数据库,代码可能因各种原因过早中止,因此您还应该在队列和处理状态查询逻辑中构建某种超时机制。
这涵盖了一次一个,以及确保任务完成的优先级顺序,但它没有解决回调问题。
回调客户端,或通知他们任务已经完成实际上超出了这里的范围,我们已经涵盖了一些选项,如果您的客户端可以轮询队列,只需在队列记录上设置一个标志就足够了以及确定它正在等待的进程是否已完成。从这个客户端来看,这可能看起来是同步的,但实际上并非如此。
根据您的情况,您可能根本不需要计时机制,并且可以从客户端驱动处理。首先,客户端提交一个将任务放入队列的调用,然后它重复调用处理器端点,该端点将返回一个“等待”响应,同一记录的另一个任务正在挂起(已从另一个客户端调用),或者它将开始下一个排队的任务。当该响应返回时,客户端检查其任务是否已完成,如果未完成,则再次调用。
你能看到这里有不同的工作部分是如何进行的,以及在不同的可能位置注入代码来定义、管理和执行这个队列概念,很难找到一个简单的方法来记录吗?
计算中的队列
无论如何,队列的概念并不新鲜,实际上它是我们理解用户界面设计的基础。在您的用户应用程序中,您的代码将发出一系列请求来执行操作,尽管它通常被抽象出来,但您可以选择在 UI 线程、随机可用的或特定的线程上处理您的请求。您还可以指定不同的优先级。
许多语言或默认实现会尝试同步执行,但要知道操作系统或较低级别的内核仍会实现某种排序队列,以允许多个应用程序和进程同时运行。在 Windows 中,这最终是消息泵。
基于云的队列
有许多基于云的高度强大的队列机制可用,提供高可用性、FIFO 和一次性交付选项。随着物联网的发展,现在有很多生产就绪的低成本实现风格,它们直接支持 .NET 和代码示例。
免责声明:我与 MS 没有任何关联,但 Azure 是我选择的个人云供应商,我认为许多其他供应商将提供相同或类似的产品和服务,但由于我不了解他们,我没有在这里直接引用它们
一个简单的起点是Azure 队列存储。这是一种有效的模式,可以一次管理一个问题以及问题的FIFO方面。如果您从之前的建议开始,首先将工作负载拆分为Request, Detect, Process,React事件链,那么基于云的队列可以帮助您完成Request和Detect阶段,但它实际上是处理的编组/编排,因此它取代了之前的
忽略名称的存储方面,它是一个简单的队列实现,名称反映了技术堆栈和提供服务的许可模型。
如果您需要更多自定义选项,或者您最终有很多队列,或者您转向更多并发处理,那么Azure 服务总线主题和订阅提供的发布-订阅模式可能更合适。
您在这条道路上走多远将取决于您的预算、任务频率、处理的并发性以及您根据业务需求调整这些工具的能力。
有趣的服务,以调查在Azure的事件枢纽和事件电网,这是关于如何在它们之间进行选择的MS指导-事件电网是其目的是简化代码的相互作用和概念复制我们使用的方法中的最新产品events,并event handlers在我们的节目,这意味着您不需要中间处理器主机,客户端应用程序可以有效地引发其他客户端中的事件...... SO 上的这个答案是一个不错的总结
什么不该做
您已经列出了一些您已经考虑并放弃的选项,下表总结了常见的解决方案及其适用性,X意味着它受支持,-意味着您可能在技术上能够使其工作,但您可能不应该尝试,而不是在此环境类型;)
| 解决方案 | 多线程 | 多进程 | 多服务器 | 锁 | 队列 | 打回来 |
|---|---|---|---|---|---|---|
.Netasync await或 (System.Threading) |
X | X | —— | X | ||
。网 lock |
X | X | ||||
.NetQueue或 (System.Collections.Concurrent) |
X | X | ||||
| 传统文件系统锁 | X | X | —— | X | ||
| *数据库行级锁 | X | X | X | X | ||
| 云或分布式文件系统锁 | X | X | X | X |
- 数据库行级锁
这通常被视为解决这些类型问题的灵丹妙药,因为数据库以非常可靠的方式实现了锁,但是该实现经常会中断其他程序流程或至少只解决了锁问题,您仍然需要分别管理队列和回调。
以上许多技术可以一起部署来实现您的需求,但是单独使用其中任何一种技术都无法解决这个问题。