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" )
可能原因
- 竞态条件:DELETE请求先返回响应,但实际数据库事务可能未完成;PATCH请求虽发送较晚,但可能在DELETE事务提交前执行,导致被删除的项目被“恢复”。
- 缓存问题:Django QuerySet的缓存、数据库层面的缓存,或Cloud Run负载均衡的响应缓存,导致GET请求读取了旧数据。
- 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。
二、消除缓存影响
- 禁用Django QuerySet缓存:
ModelViewSet默认的queryset会被缓存,重写get_queryset()方法确保每次请求都重新查询:class ProjectViewSet(ModelViewSet): # ... 原有代码 ... queryset = Project.objects.none() # 禁用默认缓存的queryset def get_queryset(self): return Project.objects.all() # 每次请求生成新的QuerySet - 强制禁用响应缓存:
在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 - 检查数据库缓存:如果使用了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
相关产品推荐
相关产品推荐

