Django:如何以线程安全的方式使用get_or_create()?

27 投票
4 回答
11301 浏览
提问于 2025-04-16 20:52

在我的Django应用中,我经常需要做一些类似于get_or_create()的事情。例如,

用户提交一个标签。需要检查一下这个标签是否已经在数据库里。如果没有,就创建一个新的记录。如果已经存在,那就更新现有的记录。

但是查看get_or_create()的文档后,我发现它似乎不是线程安全的。也就是说,线程A检查时发现记录X不存在。然后线程B也检查,发现记录X也不存在。这样,线程A和线程B都会创建一个新的记录X。

这种情况肯定很常见。我该如何以线程安全的方式来处理呢?

4 个回答

3

试试使用 transaction.commit_on_success 装饰器来处理你在调用 get_or_create(**kwargs) 时的情况。

“使用 commit_on_success 装饰器可以让你在一个函数中完成的所有工作都在一个事务里。如果这个函数顺利执行完,Django 就会在那个时刻把所有的工作都保存到数据库里。但如果函数出错了,Django 就会撤销这次事务。”

除此之外,当多个线程同时调用 get_or_create 时,它们都会尝试用传入的参数去获取对象(除了 "defaults" 参数,这个参数是一个字典,用于在 get() 失败时创建对象)。如果获取失败,两个线程都会尝试创建对象,这样就可能会出现多个重复的对象,除非在数据库层面上对 get() 调用中使用的字段实现了唯一性约束。

这和这个帖子很相似:我该如何处理 Django 中的竞争条件?

51

自2013年左右,get_or_create这个方法变得原子化了,这样它在处理并发时表现得很好:

这个方法是原子性的,前提是你正确使用它,数据库配置也正确,底层数据库的行为也正常。不过,如果在数据库层面没有对get_or_create调用中使用的参数进行唯一性约束(可以查看unique或unique_together),那么这个方法就可能会出现竞争条件,导致同时插入多条相同参数的记录。

如果你使用的是MySQL,确保使用READ COMMITTED的隔离级别,而不是默认的REPEATABLE READ,否则你可能会遇到get_or_create抛出IntegrityError的情况,但在后续的get()调用中却找不到这个对象。

来源: https://docs.djangoproject.com/en/dev/ref/models/querysets/#get-or-create

下面是一个你可以如何实现的例子:

定义一个模型,使用unique=True:

class MyModel(models.Model):
    slug = models.SlugField(max_length=255, unique=True)
    name = models.CharField(max_length=255)

MyModel.objects.get_or_create(slug=<user_slug_here>, defaults={"name": <user_name_here>})

... 或者使用unique_together:

class MyModel(models.Model):
    prefix = models.CharField(max_length=3)
    slug = models.SlugField(max_length=255)
    name = models.CharField(max_length=255)

    class Meta:
        unique_together = ("prefix", "slug")

MyModel.objects.get_or_create(prefix=<user_prefix_here>, slug=<user_slug_here>, defaults={"name": <user_name_here>})

注意,非唯一字段应该放在defaults字典中,而不是放在get_or_create的唯一字段中。这将确保你的创建操作是原子性的。

这是在Django中如何实现的: https://github.com/django/django/blob/fd60e6c8878986a102f0125d9cdf61c717605cf1/django/db/models/query.py#L466 - 尝试创建一个对象,捕获可能出现的IntegrityError,并在这种情况下返回副本。换句话说:在数据库中处理原子性。

11

这应该是一个非常常见的情况。我该如何以线程安全的方式处理呢?

没错。

在SQL中,通常的做法就是直接尝试创建记录。如果成功了,那就太好了,继续进行。

如果在创建记录时遇到“重复”错误,那就进行一次查询,然后继续。

不过,Django有一个ORM层,还有自己的缓存。所以它的处理逻辑有所不同,常见的情况可以直接快速处理,而不常见的情况(比如重复)则会抛出一个少见的错误。

撰写回答