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

asn*_*zin 0 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())
Run Code Online (Sandbox Code Playgroud)

这是我的模型:

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)
Run Code Online (Sandbox Code Playgroud)

我正在尝试并行插入 ManyToMany 关系表。显然,由于 READ COMMITED 隔离级别的原因,这会导致错误:

django.db.utils.IntegrityError: duplicate key value violates unique constraint
Run Code Online (Sandbox Code Playgroud)

绝对没问题,但是如何彻底消除这个错误呢?

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

image = Image.objects.select_for_update().get(pk=2)
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

knb*_*nbk 5

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

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

如果您锁定的是Tag而不是锁定,这也可以工作,但如果某些代码锁定,而其他代码锁定 ,则Image它不起作用。此时,一个进程可以获取 上的锁,但另一个进程不会等待,因为它仍然可以获取 上的锁,并且两个进程同时尝试将同一行插入到关系表中。TagImageImageTag

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