You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django实现无竞态条件的字符串值递增问题求助

问题描述

我希望在保存文件夹时生成类似name, name(1), name(2)的名称,为此实现了如下代码:

with transaction.atomic():
    same_name_count = Folder.objects.filter(
        owner=validated_data["owner"],
        name__iregex=r"%s(\s\(\d+\))?" % validated_data["name"],
    ).count()

    if same_name_count:
        validated_data["name"] = f"{validated_data['name']} ({same_name_count+1})"

    folder = Folder.objects.create(**validated_data)

但在Celery任务中运行该代码时仍出现竞态条件,导致生成重复名称。我也曾尝试使用select_for_update锁定行以获取正确计数,但问题依旧。我的Folder模型定义如下:

class Folder(models.Model):
    owner = models.ForeignKey('User')
    name = models.CharField(max_length=255)

请帮忙解决该竞态条件问题。

解决方案

核心问题分析

当前写法在Celery多任务并行场景下失效,原因在于:

  • select_for_update未正确使用:仅查询计数但无匹配行时,没有任何行被锁定,并发任务仍能同时执行查询逻辑。
  • 查询计数与创建操作并非原子性的“获取唯一名称+写入”流程,多个任务可能同时拿到相同的计数结果,进而生成重复名称。

可行方案

方案1:数据库唯一约束+重试机制

这是最可靠的方案,利用数据库的唯一约束拦截重复,通过重试生成正确名称:

  1. 给Folder模型添加联合唯一约束:
class Folder(models.Model):
    owner = models.ForeignKey('User', on_delete=models.CASCADE)
    name = models.CharField(max_length=255)

    class Meta:
        # Django 2.2以下写法
        unique_together = ('owner', 'name')
        # Django 2.2+推荐写法
        # constraints = [
        #     models.UniqueConstraint(fields=['owner', 'name'], name='unique_owner_folder_name')
        # ]
  1. 修改创建逻辑,加入重试机制:
from django.db import IntegrityError
import random
import time

def create_unique_folder(validated_data):
    base_name = validated_data["name"]
    owner = validated_data["owner"]
    attempt = 0
    max_attempts = 5  # 限制最大重试次数,避免死循环

    while attempt < max_attempts:
        try:
            with transaction.atomic():
                # 先检查基础名称是否存在,减少不必要的重试
                if not Folder.objects.filter(owner=owner, name=base_name).exists():
                    return Folder.objects.create(**validated_data)
                
                # 提取现有带序号的文件夹的最大序号
                from django.db.models import Func, F, Value
                from django.db.models.fields import IntegerField

                class ExtractNumber(Func):
                    function = 'REGEXP_REPLACE'
                    template = "%(function)s(%(expressions)s, '.*\\((\\d+)\\)$', '\\1')"
                    output_field = IntegerField()

                max_num = Folder.objects.filter(
                    owner=owner,
                    name__iregex=r"%s\s\(\d+\)$" % base_name
                ).annotate(
                    num=ExtractNumber(F('name'))
                ).aggregate(max_num=models.Max('num'))['max_num'] or 0

                validated_data["name"] = f"{base_name} ({max_num + 1})"
                return Folder.objects.create(**validated_data)
        except IntegrityError:
            # 触发唯一约束,说明有并发任务先创建了该名称,重试
            attempt += 1
            time.sleep(random.uniform(0.1, 0.5))  # 随机休眠降低冲突概率
    raise Exception("无法创建唯一名称的文件夹,重试次数已达上限")

核心逻辑:数据库兜底拦截重复,出现冲突时自动重试,确保最终生成唯一名称。

方案2:用户级行锁串行化操作

通过锁定用户对应的专属锁行,确保同一用户的文件夹创建操作串行执行:

  1. 新增锁模型:
class UserFolderLock(models.Model):
    user = models.OneToOneField('User', on_delete=models.CASCADE, primary_key=True)
    # 仅用于占位加锁,无需额外字段
  1. 修改创建逻辑:
from django.db import transaction

with transaction.atomic():
    # 获取或创建用户的锁行,select_for_update确保锁定,同一用户的并发任务会排队
    lock, _ = UserFolderLock.objects.select_for_update().get_or_create(user=validated_data["owner"])
    
    base_name = validated_data["name"]
    same_name_count = Folder.objects.filter(
        owner=validated_data["owner"],
        name__iregex=r"%s(\s\(\d+\))?" % base_name,
    ).count()

    if same_name_count:
        validated_data["name"] = f"{base_name} ({same_name_count+1})"

    folder = Folder.objects.create(**validated_data)

这个方案通过锁定用户对应的唯一行,强制同一用户的文件夹创建操作串行,彻底避免竞态。需注意:数据库需支持行锁(如PostgreSQL、MySQL InnoDB)。

为什么之前的select_for_update无效?

之前的select_for_update仅作用于Folder的查询结果,当没有匹配的文件夹(如首次创建)时,没有任何行被锁定,并发任务仍能同时进入查询流程,拿到相同的计数结果,导致重复名称。而方案2锁定的是用户的锁行,无论是否存在文件夹,都会强制串行。

内容的提问来源于stack exchange,提问作者Arti

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 19:47:37