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

如何避免Django视图因执行时间过长被重复调用?

问题分析

你遇到的重复调用问题,核心原因几乎都是请求超时触发的重试机制:

  • 客户端(浏览器/前端)默认超时时间较短,当请求耗时超过阈值时会主动重试;
  • WSGI服务器(如Gunicorn)的超时设置过短,会中断长耗时请求并重启处理;
  • 同一id的请求没有互斥机制,导致重复执行子进程。

以下是不用Celery等异步框架的解决方案:


解决方案1:给请求加跨进程互斥锁(基于ID)

利用文件锁实现跨进程的请求互斥,确保同一id的请求同一时间只有一个在执行,后续重复请求直接返回"处理中"状态。

修改后的视图代码:

import fcntl
import os
from django.http import JsonResponse
from time import perf_counter
import logging

log = logging.getLogger(__name__)

def process_images(request, id):
    lock_file_path = f"{settings.MEDIA_ROOT}/{id}/processing.lock"
    # 确保锁文件目录存在
    os.makedirs(os.path.dirname(lock_file_path), exist_ok=True)
    
    with open(lock_file_path, 'w') as lock_file:
        try:
            # 尝试获取非阻塞排他锁
            fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            # 已有进程在处理该ID,直接返回处理中状态
            return JsonResponse({"status": "processing"}, status=202)
        
        log.debug("Processing images view...")
        start = perf_counter()
        try:
            # 执行远程处理脚本
            result = subprocess.run(
                ["ssh", "<REMOTE SERVER>", "bash", "process_images.sh"],
                capture_output=True,
                text=True
            )
            log.debug("Result stdout:\n" + result.stdout)
            
            # 读取处理结果
            with open(f"{settings.MEDIA_ROOT}/{id}/image.json", "r") as f:
                response_data = json.load(f)
                
            time_took = perf_counter() - start
            log.debug(f"View time took {time_took} seconds")
            return JsonResponse(response_data, status=201)
        finally:
            # 释放锁并清理锁文件
            fcntl.flock(lock_file, fcntl.LOCK_UN)
            os.unlink(lock_file_path)

解决方案2:调整服务器与客户端超时设置

如果是超时导致的重试,直接延长超时时间可以从根源解决:

  • WSGI服务器调整:比如Gunicorn启动时添加--timeout 300(设置超时为300秒,根据实际处理时间调整),避免服务器主动中断长耗时请求;
  • 客户端调整:如果是前端发起的请求,修改前端请求的超时参数(如Axios的timeout配置),确保超时时间大于实际处理时长。

解决方案3:提前返回响应+客户端轮询

如果长连接超时无法避免,可以将请求拆分为"提交任务"和"查询结果"两步,避免长时间占用连接:

1. 修改视图代码

import fcntl
import os
import threading
from django.http import JsonResponse
import subprocess
import logging

log = logging.getLogger(__name__)

def process_images(request, id):
    lock_file_path = f"{settings.MEDIA_ROOT}/{id}/processing.lock"
    os.makedirs(os.path.dirname(lock_file_path), exist_ok=True)
    
    with open(lock_file_path, 'w') as lock_file:
        try:
            fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            return JsonResponse({"status": "processing", "id": id}, status=202)
        
        # 非阻塞启动远程处理进程
        process = subprocess.Popen(
            ["ssh", "<REMOTE SERVER>", "bash", "process_images.sh"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        
        # 后台线程等待进程完成并清理锁
        def wait_and_cleanup():
            try:
                process.wait()
                log.debug("Remote processing completed")
            finally:
                os.unlink(lock_file_path)
        
        threading.Thread(target=wait_and_cleanup, daemon=True).start()
        return JsonResponse({"status": "accepted", "id": id}, status=202)

# 新增查询结果视图
def get_process_result(request, id):
    json_path = f"{settings.MEDIA_ROOT}/{id}/image.json"
    lock_path = f"{settings.MEDIA_ROOT}/{id}/processing.lock"
    
    if os.path.exists(lock_path):
        return JsonResponse({"status": "processing"}, status=202)
    if os.path.exists(json_path):
        with open(json_path, 'r') as f:
            response_data = json.load(f)
        return JsonResponse(response_data, status=200)
    return JsonResponse({"status": "not_found"}, status=404)

2. 更新urls.py

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<id>/process', views.process_images, name='process'),
    path('<id>/result', views.get_process_result, name='result'),  # 新增结果查询路由
    path('<id>/', views.model, name='model')
]

app_name = "render"

客户端逻辑改为:先调用/process获取202响应,然后每隔一段时间轮询/result接口,直到拿到最终处理结果。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 11:45:38