Django:多次到多次的IntegrityError add()

gue*_*tli 14 python django postgresql race-condition

我们在django中遇到了一个已知问题:

多次到多次添加()中的IntegrityError

如果多个进程/请求尝试将同一行添加到ManyToManyRelation,则存在竞争条件.

如何解决这个问题?

Envionment:

  • Django 1.9
  • Linux服务器
  • Postgres 9.3(如有必要,可以进行更新)

细节

如何重现它:

my_user.groups.add(foo_group)
Run Code Online (Sandbox Code Playgroud)

如果两个请求尝试一次执行此代码,则上述操作失败.这是数据库表和失败的约束:

myapp_egs_d=> \d auth_user_groups
  id       | integer | not null default ...
  user_id  | integer | not null
  group_id | integer | not null
Indexes:
           "auth_user_groups_pkey" PRIMARY KEY, btree (id)
fails ==>  "auth_user_groups_user_id_group_id_key" UNIQUE CONSTRAINT,
                                            btree (user_id, group_id)
Run Code Online (Sandbox Code Playgroud)

环境

由于这只发生在生产机器上,并且我的上下文中的所有生产机器都运行postgres,因此仅使用postgres解决方案.

e4c*_*4c5 10

错误可以复制吗?

是的,让我们使用Django文档中的着名PublicationArticle模型.然后,让我们创建一些线程.

import threading
import random

def populate():

    for i in range(100):
        Article.objects.create(headline = 'headline{0}'.format(i))
        Publication.objects.create(title = 'title{0}'.format(i))

    print 'created objects'


class MyThread(threading.Thread):

    def run(self):
        for q in range(1,100):
            for i in range(1,5):
                pub = Publication.objects.all()[random.randint(1,2)]
                for j in range(1,5):
                    article = Article.objects.all()[random.randint(1,15)]
                    pub.article_set.add(article)

            print self.name


Article.objects.all().delete()
Publication.objects.all().delete()
populate()
thrd1 = MyThread()
thrd2 = MyThread()
thrd3 = MyThread()

thrd1.start()
thrd2.start()
thrd3.start()
Run Code Online (Sandbox Code Playgroud)

您肯定会看到错误报告中报告的类型的唯一键约束违规.如果您没有看到它们,请尝试增加线程数或迭代次数.

有工作吗?

是.使用through模型和get_or_create.这是根据django文档中的示例改编的models.py.

class Publication(models.Model):
    title = models.CharField(max_length=30)

    def __str__(self):              # __unicode__ on Python 2
        return self.title

    class Meta:
        ordering = ('title',)

class Article(models.Model):
    headline = models.CharField(max_length=100)
    publications = models.ManyToManyField(Publication, through='ArticlePublication')

    def __str__(self):              # __unicode__ on Python 2
        return self.headline

    class Meta:
        ordering = ('headline',)

class ArticlePublication(models.Model):
    article = models.ForeignKey('Article', on_delete=models.CASCADE)
    publication = models.ForeignKey('Publication', on_delete=models.CASCADE)
    class Meta:
        unique_together = ('article','publication')
Run Code Online (Sandbox Code Playgroud)

这是新的线程类,它是上面的一个修改.

class MyThread2(threading.Thread):

    def run(self):
        for q in range(1,100):
            for i in range(1,5):
                pub = Publication.objects.all()[random.randint(1,2)]
                for j in range(1,5):
                    article = Article.objects.all()[random.randint(1,15)]
                    ap , c = ArticlePublication.objects.get_or_create(article=article, publication=pub)
            print 'Get  or create', self.name
Run Code Online (Sandbox Code Playgroud)

你会发现异常不再显示.随意增加迭代次数.我只用了1000 get_or_create就没有抛出异常.但是add()通常会在20次迭代中抛出异常.

为什么这样做?

因为get_or_create是原子的.

假设正确使用,正确的数据库配置以及底层数据库的正确行为,此方法是原子的.但是,如果在数据库级别没有为get_or_create调用中使用的kwargs强制执行唯一性(请参阅unique或unique_together),则此方法容易出现竞争条件,这可能导致多行同时插入相同的参数.

更新: 感谢@louis指出直通模型实际上可以被淘汰.使用get_or_createMyThread2可以改为.

ap , c = article.publications.through.objects.get_or_create(
            article=article, publication=pub)
Run Code Online (Sandbox Code Playgroud)

  • `get_or_create()`在这里得到修复:https://github.com/django/django/commit/1b331f6c1e ...对于ManyToMany的`add()`也很好.非常感谢你的回答. (2认同)