我使用Java Servlets实现了一个Web服务。
我得到了以下设置: 有一个数据库可以处理“工作”条目。每项工作都有“执行”等状态。或者排队'或者'完成了#39;如果用户启动新作业,则会在数据库中创建一个条目,其中包含作业和队列中的状态'。
只有在已经执行的作业少于五个的情况下才能执行作业。如果还有其他五个人已经在执行状态,则需要留在队列中。并且Cronjob稍后将处理此工作的执行。
现在我只是想知道,如果此时执行的作业少于五个,我的脚本将执行此作业。但是,如果同时在我的脚本询问数据库正在执行多少作业和开始执行作业的脚本之间,另一个用户的另一个请求创建了一个作业,并且还获得了四个正在执行的作业'作为数据库的结果。
然后会有竞争条件,并且会执行6个工作。
我该怎样防止这样的事情? 有什么建议?非常感谢你!
答案 0 :(得分:7)
如果我理解正确并且您可以控制向DB发出请求的应用程序层,则可以使用信号量控制谁正在访问数据库。
在某种程度上,信号量就像红绿灯一样。它们只能访问N个线程的关键代码。因此,您可以将N设置为5,并且只允许关键代码中的线程将其状态更改为executing
等。
Here是一个关于使用它们的好教程。
答案 1 :(得分:3)
您可以使用记录锁定来控制并发。一种方法是执行"选择更新"查询。
您的应用程序必须具有存储worker_count的其他表。然后您的servlet必须执行以下操作:
获取数据库连接
关闭自动提交
使用' IN QUEUE'插入作业状态
执行"从...中选择worker_cnt进行更新"查询。
(此时执行相同查询的其他用户必须等到我们提交)
阅读worker_cnt值
如果worker_cnt> = 5提交并退出。
(此时您获得执行作业的票证,但其他用户仍在等待)
将作业更新为'执行'
增量worker_cnt
提交。
(此时其他用户可以继续查询并获得更新的worker_cnt)
执行作业
将作业更新为'已完成'
减少worker_cnt
再次提交
关闭数据库连接
答案 2 :(得分:1)
编辑:我现在明白你的问题。 我做了另一个回复:)
是的,你可能有竞争条件。 您可以使用数据库锁来处理它们。 如果通常不以并发方式访问记录,请查看悲观锁。 如果通常以并发方式访问记录,请查看乐观锁。
答案 3 :(得分:1)
Guy Grin是对的,你所要求的是一种可以用semaphores解决的互斥情况。 Dijkstra的这个结构可以解决你的问题。
此构造通常用于代码,一次只能由一个进程执行。示例情况正是您面临的情况;例如数据库事务,需要确保您不会遇到丢失的更新或脏读。为什么要同时执行5次?当你允许同时执行时,你确定你没有遇到这些问题吗?
基本思想是在代码中有一个所谓的关键部分,必须保护其免受竞争条件的影响。需要互斥处理。代码的这一部分被标记为关键,并且在执行之前告诉其他方也想要将其调用wait()
。一旦完成它的魔法,它就会调用notify()
,内部处理程序现在允许下一个进程在线执行临界区。
可是:
我强烈建议您不要自行实施 ANY 互斥处理方法。在几年前的理论计算机科学课上,我们在操作系统级别上分析了这些结构并证明了可能出错的地方。乍一看它看起来很简单,除了眼睛之外还有更多的东西,根据语言,如果你自己做的话,很难做到正确。特别是在Java和相关语言中,您无法控制底层VM正在执行的操作。相反,预先实施的开箱即用解决方案已经过测试并证明是正确的。
在生产环境中处理互斥之前,请先阅读一下,并确保了解它的含义。例如。有The Little Book of Semaphores这是一个写得很好,阅读很好的参考文献。至少瞥了一眼。
我对Java Servlets不太确定,但Java确实为一个名为synchronized
的关键字中的互斥提供了开箱即用的解决方案,以标记代码中不允许执行的关键部分同时通过几个过程。不需要外部库。
早期帖子this上提供了一个不错的示例代码。虽然已经说明了,但如果你处理好几个生产者/消费者,我会提醒你真正使用notifyAll()
,否则会发生奇怪的事情,并且饥饿中的疯狂过程将会杀死你的猫。
可以找到关于该主题的另一个更大的教程here。
答案 4 :(得分:1)
正如其他人的回应,这种情况需要信号量或互斥量。我认为你可能要小心的一个领域是,权威的互斥体在哪里生活。根据具体情况,您可以有几种不同的最佳解决方案(权衡安全性与性能/复杂性):
a)如果您只有一个服务器(非集群),并且修改数据库的唯一用例是通过您的Servlet,那么您可以实现静态内存互斥(一些可以同步的常见对象)访问反对)。这对性能影响最小,并且最容易维护(因为所有相关代码都在您的项目中)。此外,它并不依赖于您正在使用的特定数据库的特性。它还允许您锁定对非数据库对象的访问。
b)如果您将拥有多个单独的服务器,但它们都是您的代码实例,您可以实现同步服务,允许特定实例在允许之前获取锁定(可能具有超时)更新数据库。这将有点复杂,但仍然所有逻辑都将驻留在您的代码中,并且该解决方案可以跨数据库类型移植。
c)如果您的数据库可以由您的服务器或不同的后端进程(例如ETL)更新,那么唯一的方法是在数据库中实现记录级别锁定。如果这样做,您将依赖于数据库提供的特定类型的支持,如果您碰巧移植到其他数据库,则可能需要更改。在我看来,这是最复杂,最不易维护的选择,只有在c)的条件明确无误时才应该采用。
答案 5 :(得分:1)
答案隐含在你的问题中:你的请求必须排队,以便与生产者和消费者建立一个fifo队列。
servlet总是在队列中添加作业(可选择检查它是否已满),其他5个线程将一次提取一个作业,或者如果队列为空则休眠。
没有必要为此使用cron或mutex,只记得同步队列或消费者可以两次提取相同的作业。
答案 6 :(得分:0)
在我看来,即使你不使用ExecutorService,如果你总是更新数据库并从单线程开始你的工作,最容易实现你的逻辑。您可以在队列中安排作业的执行,并让一个线程执行并将数据库状态更新为正确的表单。
如果要控制执行的作业数量。一种方法是使用ExecutorsService和FixedThreadPool为5.通过这种方式,您将确保一次只执行5个作业而不再执行...所有其他作业将在ExecutorService中排队。
我的一些同事会指出低级并发API。我认为这些不是用于修复一般编程问题。无论您决定做什么尝试使用更高级别的API,而不是深入了解细节。大多数低级别的东西已经在现有框架中实现,我怀疑你会做得更好。