【问题标题】:Django: how to do get_or_create() in a threadsafe way?Django:如何以线程安全的方式执行 get_or_create()?
【发布时间】:2011-09-29 00:23:43
【问题描述】:

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

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

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

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

【问题讨论】:

  • 两个线程之一会得到重复记录错误和异常。不会有重复的数据。

标签: python database django concurrency thread-safety


【解决方案1】:

这么多年过去了,但没有人写过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)。在这种情况下如何避免竞争条件?
  • 我认为我们需要避免在其他地方做MyModel.objects.get_or_create(**data),并始终使用此功能。我说的对吗?
  • 您可以在 QuerySet 管理器中使用线程锁覆盖 get_or_create,如下所示:code.djangoproject.com/attachment/ticket/13105/… 但是,如果您在后台任务中使用带锁的池,则此问题仅出现在池等高并发位置,例如和本地 django 视图一样,竞争条件的机会非常小。但是,如果你得到这个,你应该考虑全局锁,例如redis锁,缓存值等。
  • 避免使用它也有帮助,但如果你真的需要 get_or_create,请考虑上面的示例。请注意,您可以在视图中通过全局锁定来锁定您的工作人员,这样会降低您的网络应用程序的速度。
【解决方案2】:

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

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

如果您使用的是 MySQL,请务必使用 READ COMMITTED 隔离 level 而不是 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>})

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

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

【讨论】:

  • 感谢投票支持此答案的人,我添加了一些示例以使其更易于理解。
  • 是的,从 2011 年提出问题时发生了很多变化。所以这个答案比第一个更实际。
【解决方案3】:

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

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

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

和这个帖子很像 How do I deal with this race condition in django?

【讨论】:

  • 这实际上不是必需的,请参阅我的其他答案以获得更好的处理方法。
【解决方案4】:

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

是的。

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

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

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

【讨论】:

  • 我在 postgres 数据库中遇到了重复条目,当我在获取并发请求的视图方法中使用 get_or_create 时应该是唯一的,我认为这是一个有效的问题。跨度>
  • @A Lee:正确定义唯一索引约束后,应该不可能出现重复。你是如何绕过唯一索引约束的?
  • 啊,既然我想得更清楚了,那问题就解决了。 get_or_create 使用了多个字段,我将其移至不同的执行路径,而不是将其留在视图中并在多个模型字段中添加唯一约束。
  • @A Lee:你仍然可以修复它。
  • @S.Lott:所以当 get_or_create 尝试创建记录时遇到“重复”异常,它会自动尝试“获取”记录吗?还是我必须在我的代码中这样做?在这种情况下,Django 会抛出什么类型的异常?我在 django.core.exceptions.py 中没有看到任何“重复”异常
猜你喜欢
  • 2011-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-30
  • 1970-01-01
  • 2011-08-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多