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

Django DRF ModelViewSet请求竞态条件问题排查

解决Google Cloud Run上DRF ModelViewSet的竞态与缓存异常问题

问题场景

在Google Cloud Run部署的Django Rest Framework项目中,projects/端点(基于ModelViewSet)出现以下异常:

  • 操作时间线:
    • 14:45:09.420 发送PATCH请求
    • 14:45:12.724 发送DELETE请求
    • 14:45:12.852 收到DELETE 204响应
    • 14:45:13.263 收到PATCH 200响应
    • 14:45:13.279 发送GET请求
    • 14:45:13.714 收到GET 200响应
  • 异常表现:DELETE返回成功后立即调用GET,响应仍包含已删除的Project;延迟一段时间后再调用GET,该项目才从列表中消失。
  • 相关代码为常规实现(视图、序列化器、模型如下),另有一个模型通过外键引用Project,on_delete设为SET_NULL:
class ProjectViewSet(ModelViewSet):
    parser_classes = (MultiPartParser, FormParser, JSONParser)
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer
    pagination_class = ProjectPagination

class ProjectSerializer(serializers.ModelSerializer):
    creator = UserUUIDField(default=serializers.CurrentUserDefault())
    image = serializers.ImageField(required=False)

    class Meta:
        model = Project
        fields = "__all__"

class Project(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    creator = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    image = models.ForeignKey(
        wand_image,
        on_delete=models.DO_NOTHING,
        null=True,
        blank=True,
        related_name="projects"
    )

可能原因

  1. 竞态条件:DELETE请求先返回响应,但实际数据库事务可能未完成;PATCH请求虽发送较晚,但可能在DELETE事务提交前执行,导致被删除的项目被“恢复”。
  2. 缓存问题:Django QuerySet的缓存、数据库层面的缓存,或Cloud Run负载均衡的响应缓存,导致GET请求读取了旧数据。
  3. Cloud Run多实例一致性:无状态多实例部署下,不同实例的数据库连接状态可能不同,导致读取到未同步的数据。

解决方案

一、修复竞态条件,确保操作原子性

在视图的DELETE和PATCH操作中添加行级锁,并校验实例状态,避免并发操作冲突:

from rest_framework import status
from rest_framework.response import Response

class ProjectViewSet(ModelViewSet):
    # ... 原有代码 ...
    
    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        # 获取行锁,阻止其他操作修改该实例
        instance = Project.objects.select_for_update().get(pk=instance.pk)
        return super().destroy(request, *args, **kwargs)
    
    def update(self, request, *args, **kwargs):
        instance = self.get_object()
        # 先检查实例是否已被删除
        if not Project.objects.filter(pk=instance.pk).exists():
            return Response({"detail": "项目已删除"}, status=status.HTTP_404_NOT_FOUND)
        # 获取行锁,确保操作顺序
        instance = Project.objects.select_for_update().get(pk=instance.pk)
        return super().update(request, *args, **kwargs)

如果前端允许并发发送请求,建议在前端添加逻辑,确保DELETE操作发起前禁止PATCH,或等待PATCH完成后再执行DELETE。

二、消除缓存影响

  1. 禁用Django QuerySet缓存:
    ModelViewSet默认的queryset会被缓存,重写get_queryset()方法确保每次请求都重新查询:
    class ProjectViewSet(ModelViewSet):
        # ... 原有代码 ...
        queryset = Project.objects.none()  # 禁用默认缓存的queryset
        
        def get_queryset(self):
            return Project.objects.all()  # 每次请求生成新的QuerySet
    
  2. 强制禁用响应缓存:
    在GET列表接口中添加响应头,阻止客户端和CDN缓存结果:
    class ProjectViewSet(ModelViewSet):
        # ... 原有代码 ...
        
        def list(self, request, *args, **kwargs):
            response = super().list(request, *args, **kwargs)
            response["Cache-Control"] = "no-cache, no-store, must-revalidate"
            response["Pragma"] = "no-cache"
            response["Expires"] = "0"
            return response
    
  3. 检查数据库缓存:如果使用了Redis等外部缓存,确保删除Project后清理相关缓存键;若使用数据库物化视图,需在DELETE后执行REFRESH MATERIALIZED VIEW更新数据。

三、优化Cloud Run数据库连接配置

禁用Django的数据库连接复用,避免实例使用旧连接读取未同步的数据:
在settings.py中修改数据库配置:

DATABASES = {
    'default': {
        # ... 原有数据库配置 ...
        'CONN_MAX_AGE': 0,  # 禁用连接复用,每次请求使用新连接
    }
}

四、验证外键关联影响

检查关联模型的on_delete=SET_NULL是否同步执行:

  • 确认关联模型的外键字段在Project删除后立即被置空;
  • 若使用信号处理外键操作,确保信号是同步执行的,没有异步延迟。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 05:40:40