为什么select_for_update在并发插入中有效?

时间:2017-04-20 08:46:21

标签: python sql django postgresql orm

我有一个代码,应该在并发请求和重负载下工作。

我写了一个例子来更好地理解我试图做的事情:

def add_tag():
    with transaction.atomic():
        image = Image.objects.get(pk=2)
        tag = Tag.objects.get(pk=6)

        image.tags.add(tag) # concurrent insert

    return 'done'


class Command(BaseCommand):
    def handle(self, *args, **options):
        with ProcessPoolExecutor(max_workers=3) as executor:
            futures = []
            for _ in range(3):
                futures.append(executor.submit(add_tag))

            for future in as_completed(futures):
                print(future.result())

这是我的模特:

class Image(models.Model):
    title = models.CharField(max_length=255)
    tags = models.ManyToManyField('ov_tags.Tag')

class Tag(models.Model):
    title = models.CharField(max_length=255)

我试图并行插入ManyToMany关系表。显然,这会导致错误,因为READ COMMITED隔离级别:

django.db.utils.IntegrityError: duplicate key value violates unique constraint

绝对没问题,但如何完全删除此错误?

为了保护我的图像,我尝试在图像选择上使用select_for_update。

image = Image.objects.select_for_update().get(pk=2)

而且......它有效!我跑了好几次。没有错误,项目正确插入。但我不知道为什么?

select_for_update是否无论如何锁定关系表?或者它是否在应用程序端发生?是否有正确的方法来实现这种行为?

我可以使用空选择来锁定插入吗?

SELECT "image_tags"."tag_id" FROM "image_tags" WHERE ("image_tags"."tag_id" IN (6) AND "image_tags"."image_id" = 2) FOR UPDATE

2 个答案:

答案 0 :(得分:4)

在数据库级别,您只需锁定要添加标记的特定Image实例。您确认这不会阻止插入关系表。如果另一段代码忽略了锁,只是在关系表中插入一个新行,你仍然会遇到麻烦。

它适用于这段代码,因为每个事务都是"表现良好"。在将新条目添加到关系表之前,每个事务首先获取特定映像的锁定。这意味着执行程序池中的每个进程将在尝试在关系表中添加新行之前等待当前进程完成其事务。

如果您锁定Tag而不是Image,这也会有效,但如果某些代码锁定了Tag,它就无法工作代码锁定Image。此时,一个进程可以获取Image上的锁,但另一个进程不会等待,因为它仍然可以获取Tag上的锁,并且两个进程都尝试插入同一行同时进入关系表。

我的意思是"表现良好":应用程序的每个部分都必须以特定的方式运行(获得相同的锁定)。如果您的应用程序中只有一部分忽略了此要求,则可能会遇到竞争条件。只有当应用程序的所有部分表现良好时,您才能以这种方式防止竞争条件。

答案 1 :(得分:0)

这正是发生的事情,select_for_update调用是在数据库级别锁定Image表,因此在transaction.atomic结束之前,其他任何事务都无法修改所选行。块。

参见参考https://docs.djangoproject.com/en/1.11/ref/models/querysets/#select-for-update