Django: 如何以线程安全的方式执行get_or_create()操作?

27
在我的Django应用程序中,经常需要执行类似于get_or_create()的操作。例如:

用户提交标签。需要查看该标签是否已经在数据库中。如果不存在,则为其创建一个新记录。如果存在,则只需更新现有记录。

但是查看get_or_create()的文档时,似乎它不是线程安全的。线程A检查并发现Record X不存在。然后线程B检查并发现Record X不存在。现在线程A和线程B都将创建一个新的Record X。

这必须是一个非常普遍的情况。如何以线程安全的方式处理它?


1
其中一个线程将会收到重复记录错误和异常。不会有重复数据。 - S.Lott
4个回答

51

自2013年左右开始,get_or_create是原子性的,因此它可以很好地处理并发:

假定正确使用、正确的数据库配置和底层数据库的正确行为,此方法是原子性的。然而,如果在get_or_create调用中使用的kwargs没有在数据库级别上强制执行唯一性(请参见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_togheter:

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,在这种情况下返回副本。换句话说:在数据库中处理原子性。


3
谢谢您对这个答案进行投票,我已经添加了一些例子,使其更易于理解。 - Emil Stenström
是的,自2011年提出这个问题以来,很多事情都发生了变化。因此,这个答案比第一个更加实际。 - Andrey Shokhin

11

这应该是一个很普遍的情况,如何以线程安全的方式处理它?

是的。

在 SQL 中,“标准”解决方案是尝试创建记录。如果成功,就继续前进。

如果创建记录的尝试从 RDBMS 得到“重复”的异常,则执行 SELECT 并继续前进。

然而,Django 具有自己的 ORM 层,并且具有自己的缓存。因此,逻辑被反转,使常见情况可以直接快速地处理,不常见情况(即重复)则会引发罕见的异常。


1
我在使用视图方法获取并发请求时,在Postgres数据库中遇到了应该是唯一的重复条目,这些条目应该使用get_or_create。我认为这是一个有效的问题。 - A Lee
2
@A Lee:如果唯一索引约束正确定义,就不应该出现重复。你是如何规避唯一索引约束的? - S.Lott
啊,现在我想得更清楚了,那应该可以解决问题。get_or_create 使用了多个字段,我将其移动到了不同的执行路径上,而不是将其留在视图中并在多个模型字段上添加唯一约束。 - A Lee
@A Lee:你仍然可以修复它。 - S.Lott
@S.Lott:当get_or_create尝试创建记录时,如果出现“重复”异常,它会自动尝试“获取”记录吗?还是我必须在我的代码中执行这个操作?在这种情况下,Django会抛出什么类型的异常?我在django.core.exceptions.py中没有看到任何“重复”异常。 - Continuation
@Continuation: 底层数据库引发异常,该异常通过Django ORM传递。请尝试从命令行运行以查看发生了什么。 - S.Lott

3

尝试使用transaction.commit_on_success 装饰器来处理在调用get_or_create(**kwargs)时的可调用对象。

"使用commit_on_success装饰器,在函数中使用单个事务处理所有工作。如果函数成功返回,则Django将在该点上提交函数内完成的所有工作。但是,如果函数引发异常,则Django将回滚事务。"

除此之外,在对get_or_create进行并发调用时,两个线程都会尝试使用传递给它的参数获取对象(除了“defaults”参数外,它是在create调用中使用的字典,以防无法检索到任何对象)。如果失败,两个线程都会尝试创建对象,从而导致多个重复对象的出现,除非在数据库级别上实现了使用get()调用中使用的字段的唯一/唯一组合。

这类似于这篇文章 如何解决Django中的竞争条件?


1
这实际上是不必要的,请查看我的其他答案以了解更好的处理方法。 - Emil Stenström

3

这么多年过去了,但没有人写过关于threading.Lock的内容。如果由于遗留原因而无法进行unique together的迁移,则可以使用锁或threading.Semaphore对象。以下是伪代码:

from concurrent.futures import ThreadPoolExecutor
from threading import Lock

_lock = Lock()


def get_staff(data: dict):
    _lock.acquire()
    try:
        staff, created = MyModel.objects.get_or_create(**data)
        return staff
    finally:
        _lock.release()


with ThreadPoolExecutor(max_workers=50) as pool:
    pool.map(get_staff, get_list_of_some_data())

如果我在其他地方已经使用了 MyModel.objects.get_or_create(**data),那么如何避免竞争条件呢? - Sagar Adhikari
我认为我们需要避免在其他地方执行MyModel.objects.get_or_create(**data),并始终使用此函数来达到目的。我的理解正确吗? - Sagar Adhikari
1
您可以在QuerySet Manager中使用线程锁覆盖get_or_create,例如:https://code.djangoproject.com/attachment/ticket/13105/django-1.1.1-thread-safe.patch但是,这个问题只会出现在高并发的地方,比如池中。如果您正在使用带有锁的池来执行后台任务,并且使用本机Django视图,则出现竞争条件的可能性非常小。但是,如果您遇到了这个问题,您应该考虑全局锁,例如redis锁、缓存值等。 - Mastermind
避免使用它也会有所帮助,但如果你真的需要 get_or_create,请考虑上面的例子。注意,您可以通过视图中的全局锁来锁定您的工作程序,这可能会减慢您的Web应用程序。 - Mastermind

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接