如何处理 Web 服务中的竞争条件?

How to handle race conditions in Web Service?

我使用 Java Servlet 实现了 Web 服务。

我得到了以下设置: 有一个处理 'job' 条目的数据库。每个作业的状态类似于 'executing' 或 'in queue' 或 'finished'。如果用户开始新工作,则会在数据库中创建一个条目,其中包含工作和状态 'in queue'.

仅当已执行的其他作业少于五个时,才应执行该作业。如果有五个其他人已经在执行状态需要保持 'in queue' 并且 Cronjob 将稍后处理此作业的执行。

现在我想知道,如果此时执行的作业少于五个,我的脚本将执行这个作业。但是如果同时,在我的脚本询问数据库有多少作业正在执行和脚本开始执行作业之间,来自另一个用户的另一个请求创建了一个作业并且也得到了 'four executing jobs' 作为结果数据库。

然后会出现竞争条件,将执行 6 个作业。

我怎样才能防止这样的事情发生? 有什么建议吗?非常非常感谢!

编辑:我现在明白你的问题了。 我做另一个回应:)

是的,您可能有竞争条件。 您可以使用数据库锁来处理它们。 如果记录不是经常被并发访问,看悲观锁。 如果记录经常被并发访问,看乐观锁。

您可以使用记录锁定来控制并发。一种方法是执行 "select for update" 查询。

您的应用程序必须有其他 table 存储 worker_count。然后您的 servlet 必须执行以下操作:

  1. 获取数据库连接

  2. 关闭自动提交

  3. 插入状态为 'IN QUEUE' 的作业

  4. 执行"select worker_cnt from ... for update"查询。

(此时执行相同查询的其他用户将不得不等到我们提交)

  1. 读取worker_cnt值

  2. 如果 worker_cnt >= 5 提交并退出。

(此时你拿到了执行任务的ticket,其他用户还在等待)

  1. 更新作业为'EXECUTING'

  2. 递增worker_cnt

  3. 提交。

(此时其他用户可以继续他们的查询并将得到更新worker_cnt)

  1. 执行作业

  2. 更新作业为'FINISHED'

  3. 递减worker_cnt

  4. 再次提交

  5. 关闭数据库连接

如果我理解正确并且您可以控制向数据库发出请求的应用层,您可以使用信号量来控制谁在访问数据库。

从某种意义上说,信号量就像交通信号灯。它们只允许 N 个线程访问关键代码。因此,您可以将 N 设置为 5,并只允许关键代码中的线程将其状态更改为 executing 等。

Here 是一个很好的使用它们的教程。

Guy Grin 说得对,你要求的是可以用 semaphores. This construct by Dijkstra 解决的互斥情况应该可以解决你的问题。

此构造通常用于代码,一次只能由 一个 进程执行。示例情况正是您似乎面临的情况;例如需要确保您不会 运行 丢失更新或脏读的数据库事务。为什么要同时执行 5 个?当您完全允许同时执行时,您确定您没有 运行 完全陷入这些问题吗?

基本思想是在你的代码中有一个所谓的关键部分,必须保护它免受竞争条件的影响。需要互斥处理。您的这部分代码被标记为关键,并且在执行之前会告诉其他也想调用它的各方 wait()。一旦完成它的魔法,它就会调用 notify() 并且内部处理程序现在允许下一个进程执行临界区。

但是:

  • 我强烈建议不要自己实施 任何 互斥处理方法。几年前,在理论计算机科学 class 中,我们在 OS 水平上分析了这些结构,并证明了可能出错的地方。即使乍一看它看起来很简单,但它的意义远不止于此,而且根据语言的不同,如果您自己动手做,真的很难把它做好。特别是在 Java 和相关语言中,您无法控制底层 VM 正在做什么。取而代之的是预先实施的开箱即用的解决方案,这些解决方案已经过测试并证明是正确的。

  • 在生产环境中处理互斥之前,先阅读一下它并确保理解它的含义。例如。有 The Little Book of Semaphores 这是一个写得很好并且很容易阅读的参考。至少看一眼吧。

我不太确定 Java Servlet,但 Java 确实有一个开箱即用的解决方案,用于在名为 synchronized 的关键字中标记关键部分的互斥在您的代码中不允许多个进程同时执行。不需要外部库。

SO 上 this 之前的 post 中提供了一个很好的示例代码。尽管那里已经说明了,但让我提醒您,如果您处理多个生产者/消费者,请真正使用 notifyAll(),否则会发生奇怪的事情,并且在饥饿中旋转的疯狂进程会杀死您的猫。

可以找到关于该主题的另一个更大的教程 here

正如其他人的反应,这种情况需要信号量或互斥量。我认为您可能需要注意的一个领域是,权威互斥体位于何处。根据情况,您可能有几种不同的最佳解决方案(权衡安全性与 performance/complexity):

a) 如果您只有一个服务器(非集群),并且修改数据库的唯一用例是通过您的 Servlet,那么您可以实现一个静态内存中互斥锁(一些常见的对象,您可以同步访问)。这对性能的影响最小,并且最容易维护(因为所有相关代码都在您的项目中)。此外,它不依赖于您正在使用的特定数据库的特性。它还允许您锁定对非数据库对象的访问。

b) 如果你有几个独立的服务器,但它们都是你代码的实例,你可以实现一个同步服务,允许特定实例在它之前获得锁(可能有超时)允许更新数据库。这会稍微复杂一些,但所有逻辑仍将驻留在您的代码中,并且该解决方案将可跨数据库类型移植。

c) 如果您的数据库可以由您的服务器或不同的后端进程(例如 ETL)更新,那么唯一的方法是在数据库中实现记录级锁定。如果这样做,您将依赖于您的数据库提供的特定类型的支持,并且如果您碰巧移植到不同的数据库,则可能需要更改。在我看来,这是最复杂、最难维护的选项,只有在 c) 的条件明确为真时才应采用。

答案隐含在您的问题中:您的请求必须入队,因此构建一个包含生产者和消费者的 fifo 队列。

servlet 始终在队列中添加作业(可选地检查队列是否已满),另外 5 个线程将每次提取一个作业,如果队列为空则休眠。

不需要为此使用 cron 或 mutex,只需记住同步队列,否则消费者可能会提取相同的作业两次。

在我看来,即使您不使用 ExecutorService,如果您总是更新数据库并从单线程启动您的作业,那么实现您的逻辑也是最容易的。您可以在队列中安排作业的执行,并让一个线程执行并将数据库状态更新为正确的形式。

如果你想控制Jobs执行的数量。一种方法是使用 ExecutorsService 和 FixedThreadPool 为 5。这样你就可以确定一次只会执行 5 个作业,不会更多...所有其他作业将在 ExecutorService 中排队。

我的一些同事会向您指出低级并发 APIs。我相信这些并不是为了解决一般的编程问题。无论您决定做什么,请尝试使用更高级别 API 并且不要深入研究细节。大多数低级别的东西已经在现有框架中实现,我怀疑你会做得更好。