繁体   English   中英

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

[英]Django: how to do get_or_create() in a threadsafe way?

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

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

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

这一定是很常见的情况。 如何以线程安全的方式处理它?

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

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

如果您使用的是 MySQL,请务必使用 READ COMMITTED 隔离级别而不是 REPEATABLE READ(默认),否则您可能会看到 get_or_create 会引发 IntegrityError 但 object 不会出现在后续 get() 调用中的情况。

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

这是一个如何做到这一点的示例:

使用 unique=True 定义 model:

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>})

请注意非唯一字段如何在默认字典中,而不是在 get_or_create 中的唯一字段中。 这将确保您的创建是原子的。

Here's how it's implemented in Django: https://github.com/django/django/blob/fd60e6c8878986a102f0125d9cdf61c717605cf1/django/db/models/query.py#L466 - Try creating an object, catch an eventual IntegrityError, and return the copy in那个案子。 换句话说:处理数据库中的原子性。

这一定是很常见的情况。 如何以线程安全的方式处理它?

是的。

SQL 中的“标准”解决方案是简单地尝试创建记录。 如果它有效,那很好。 继续。

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

然而,Django 有一个 ORM 层,有自己的缓存。 因此,逻辑被反转以使常见情况直接快速地工作,而不常见情况(重复)则引发罕见的异常。

在您尝试 get_or_create(**kwargs) 的地方尝试可调用的 transaction.commit_on_success 装饰器

"Use the commit_on_success decorator to use a single transaction for all the work done in a function.If the function returns successfully, then Django will commit all work done within the function at that point. If the function raises an exception, though, Django will回滚交易。”

除此之外,在对 get_or_create 的并发调用中,两个线程都尝试获取 object 并传递给它的参数(“默认”参数除外,它是在创建调用期间使用的字典,以防 get() 无法检索任何对象)。 如果发生故障,两个线程都会尝试创建 object 导致多个重复对象,除非在数据库级别使用 get() 调用中使用的字段实现了一些唯一/唯一一起。

它类似于这篇文章How do I deal with this race condition in django?

这么多年过去了,但没有人写过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())

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM