Django中的竞争条件

0 投票
1 回答
1187 浏览
提问于 2025-04-16 13:55

在Django中,我遇到了一个严重的竞争条件问题。问题出现在两个进程同时尝试执行某个方法时。记录的日志如下:

Job 3: Candidate
Job 3: Already taken
Job 3: Candidate
Job 3: Already taken
Job 3: Candidate
Job 3: Already taken
(et cetera for 18 MB)

下面这个方法让我很头疼。需要注意的是,这个方法会一直重新运行,直到它返回False

def some_method():
    conditions = #(amongst others, excludes jobs with status EXECUTING)

    try:
        cjob = Job.objects.filter(conditions).order_by(some_fields)[0]
    except IndexError:
        return False

    print 'Job %s: Candidate' % cjob.id

    job = cjob.for_update()

    if cjob.status != job.status:
        print 'Job %s: Already taken' % cjob.id
        return True

    print 'Job %s: Starting...' % job.id

    job.status = Job.EXECUTING
    job.save()
    # Critical section

# In models.py:
class Job(models.Model):
    # ...

    def for_update(self):
        return Job.objects.raw('SELECT * FROM `backend_job` WHERE `id` = %s FOR UPDATE', (self.id, ))[0]

目前,Django没有专门的for_update方法。为了避免创建一个包含所有条件的查询(这些条件用来判断这个任务是否需要执行),我们在简单的FOR UPDATE查询之前先执行一个复杂的查询。

我不太明白这会如何导致我们看到的问题。我们先执行查询,然后有一个语句会在另一个进程持有任务锁的时候阻塞。只有在任务状态被改变后,锁才会被释放。第二个进程现在获得了锁,但任务的状态已经改变,所以它从方法中返回,之后又重新进入;但是cjob查询不会再返回同一个任务,因为它的状态现在被过滤掉了。

我是不是误解了FOR UPDATE这个条款,还是我漏掉了什么其他的东西?

需要说明的是,我使用的是MySQL和InnoDB,并且Celery不适合这个解决方案。

1 个回答

0

这个问题是通过手动更新事务来解决的。看起来在事务开始后,查询集(QuerySet)没有更新。当两个查询集同时开始,并且有一个任务在这两个查询集中都发生时,就会导致执行者出错。

在阅读了这个回答后,我想出了一个解决办法:在return True之前,先提交事务。

撰写回答