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

Django中阻塞等待Celery任务完成会导致多用户访问时站点卡顿吗?求优化方案

Django中阻塞等待Celery任务完成会导致多用户访问时站点卡顿吗?求优化方案

肯定会卡顿!你现在的写法完全浪费了Celery的异步优势——用task.wait()会让处理当前请求的Django worker进程彻底阻塞,直到Celery任务执行完毕。如果同时有多个用户上传文件,所有可用的Django worker都会被这些阻塞的请求占满,新的请求只能排队等待,站点自然就会变慢甚至无响应。

你的代码核心问题

你当前的流程是:用户上传文件 → Django启动Celery任务 → Django原地等待任务完成 → 返回结果。这本质上还是同步处理,Celery只是帮你把任务移到了另一个进程,但Django这边并没有释放资源去处理其他请求,完全没发挥异步架构的作用。


优化思路:改成异步非阻塞的「上传-轮询-下载」流程

核心就是让Django在启动Celery任务后立即返回响应,把等待的逻辑交给前端,后台任务完成后再提供下载入口。具体步骤如下:

1. 修改上传视图,返回任务ID

不再等待任务完成,启动任务后直接返回任务ID给前端,让用户知道任务正在处理。

修改后的views.py上传部分:

@csrf_protect
def main_page(request: HttpRequest):
    if request.method == "GET":
        form = FileUploadForm()  # 原GET请求代码有误,无需传POST/FILES参数
        context = {"form": form}
        return render(request, 'main/main_page.html', context)
    
    if request.method == 'POST' and request.FILES.get('file'):
        form = FileUploadForm(request.POST, request.FILES)
        if form.is_valid():
            file = request.FILES['file']
            size_limit = 2 * 1024 * 1024  # 限制2MB
            
            # 保存临时PDF文件
            with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
                if file.size > size_limit:
                    for chunk in file.chunks():
                        temp_file.write(chunk)
                else:
                    temp_file.write(file.read())
            temp_pdf_path = temp_file.name
            
            # 启动Celery任务,不等待结果
            task = tasks.convert_pdf_to_docx.delay(temp_pdf_path)
            
            # 返回任务ID和原文件名,供前端后续使用
            return JsonResponse({
                "task_id": task.id,
                "original_filename": str(file).replace(".pdf", "")
            })
        else:
            context = {"form": form, "submit_error": True}
            return render(request, 'main/main_page.html', context)

2. 新增任务状态查询视图

用来让前端定期查询任务是否完成,以及获取下载链接:

from celery.result import AsyncResult

def check_task_status(request: HttpRequest):
    task_id = request.GET.get('task_id')
    if not task_id:
        return JsonResponse({"error": "缺少task_id参数"}, status=400)
    
    task = AsyncResult(task_id)
    if task.state == 'SUCCESS':
        return JsonResponse({
            "state": task.state,
            "download_url": f"/download/{task_id}/"
        })
    elif task.state == 'FAILURE':
        return JsonResponse({
            "state": task.state,
            "error": str(task.info)
        })
    else:
        # 任务仍在处理中,返回当前状态(如PENDING、STARTED)
        return JsonResponse({"state": task.state})

3. 新增下载视图

当任务完成后,提供文件下载,并在下载后清理临时文件:

def download_converted_file(request: HttpRequest, task_id):
    task = AsyncResult(task_id)
    if task.state != 'SUCCESS':
        return HttpResponse("任务未完成或执行失败", status=400)
    
    temp_docx_path = task.result
    original_filename = request.GET.get('original_filename', 'converted_file')
    
    try:
        with open(temp_docx_path, 'rb') as docx_file:
            file_data = docx_file.read()
            response = HttpResponse(file_data, content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document')
            response['Content-Disposition'] = f'attachment; filename={original_filename}.docx'
            response['Content-Length'] = len(file_data)
            return response
    finally:
        # 下载完成后清理临时文件
        if os.path.exists(temp_docx_path):
            os.remove(temp_docx_path)
        pdf_path = temp_docx_path.replace(".docx", ".pdf")
        if os.path.exists(pdf_path):
            os.remove(pdf_path)

4. 前端配合(简单示例)

在页面中添加JavaScript,实现上传后自动轮询任务状态:

<script>
document.querySelector('form').addEventListener('submit', async function(e) {
    e.preventDefault();
    const formData = new FormData(this);
    const response = await fetch(window.location.href, {
        method: 'POST',
        body: formData
    });
    const result = await response.json();
    
    if (result.task_id) {
        // 显示转换中提示
        const statusBox = document.createElement('div');
        statusBox.style.padding = '10px';
        statusBox.textContent = '文件转换中,请稍候...';
        document.body.appendChild(statusBox);
        
        // 每秒轮询一次任务状态
        const pollTimer = setInterval(async () => {
            const statusRes = await fetch(`/check-task-status/?task_id=${result.task_id}`);
            const statusData = await statusRes.json();
            
            if (statusData.state === 'SUCCESS') {
                clearInterval(pollTimer);
                statusBox.textContent = '转换完成!';
                // 添加下载按钮
                const downloadBtn = document.createElement('a');
                downloadBtn.href = `${statusData.download_url}?original_filename=${result.original_filename}`;
                downloadBtn.textContent = '点击下载DOCX文件';
                downloadBtn.style.display = 'block';
                downloadBtn.style.marginTop = '10px';
                document.body.appendChild(downloadBtn);
            } else if (statusData.state === 'FAILURE') {
                clearInterval(pollTimer);
                statusBox.textContent = `转换失败:${statusData.error}`;
            }
        }, 1000);
    }
});
</script>

5. 额外优化建议

  • 临时文件管理:不要依赖系统临时文件,建议用Django的FileStorage(本地或云存储),避免服务器重启或临时文件自动清理导致文件丢失。
  • Celery任务配置:给任务添加超时时间(如@shared_task(time_limit=300)),避免任务无限挂起;添加重试机制处理转换失败的情况。
  • Worker并发调整:你用了gevent作为并发模型,适合IO密集型任务,可以根据服务器配置调整--concurrency参数,增加worker数量提升并发处理能力。

这样修改后,Django worker不会再被阻塞,能同时处理大量用户的上传请求,Celery在后台异步处理转换任务,用户体验也更好——不会看到页面一直加载,而是有明确的状态提示。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:58:05