Django+FFmpeg视频转码服务优化:并发与数据库同步问题问询
优化Django+FFmpeg视频转码的并发稳定性与流程可靠性方案
嘿,咱们来一步步解决这个问题——你的代码在单用户场景下能跑,但高并发和可靠性这块确实有不少可以优化的地方,我给你拆解下需要改的关键点:
一、用异步任务队列解耦转码与请求响应
当前代码是在请求处理线程里同步执行FFmpeg,多用户并发时会把所有请求都堵在转码环节,Django的worker线程被占满后新请求就会超时。必须把转码逻辑放到异步任务队列里,比如用Celery(配合Redis/RabbitMQ当消息中间件),让请求快速返回,转码任务在后台异步执行。
示例Celery任务代码
# tasks.py from celery import shared_task import os import subprocess from django.conf import settings from your_app.models import Convert, Video # 替换成你实际的模型 @shared_task(bind=True, max_retries=3) def transcode_video_task(self, video_id, input_path, output_folder): # 定义要转码的分辨率参数 resolutions = [ ("240p", "426x240"), ("360p", "640x360"), ("480p", "854x480") ] base_output_path = os.path.join(settings.MEDIA_ROOT, output_folder) try: # 获取主视频记录 rec = Video.objects.get(id=video_id) for res_name, res_size in resolutions: output_file = f"{res_name}.mp4" output_path = os.path.join(base_output_path, output_file) # 用列表形式传FFmpeg命令,避免shell注入,同时兼容带空格的路径 command = [ "ffmpeg", "-i", input_path, "-s", res_size, "-c:v", "libx264", # 明确指定H.264编码器,保证兼容性 "-crf", "23", # 画质与文件大小的平衡值(23为默认,越小画质越好) "-preset", "medium",# 编码速度与压缩率的平衡选项 "-y", # 覆盖已存在的输出文件 output_path ] # 执行命令并严格检查返回状态 subprocess.run(command, check=True, capture_output=True, text=True) # 只有转码成功,才写入数据库 Convert.objects.get_or_create( vid=rec, name=output_file, video=os.path.join(output_folder, output_file) # 存相对路径,配合MEDIA_URL使用 ) except Video.DoesNotExist: # 视频记录不存在时重试 self.retry(exc=Exception(f"Video {video_id} not found"), countdown=60) except subprocess.CalledProcessError as e: # 转码失败,记录错误日志并重试 print(f"FFmpeg转码失败: {e.stderr}") self.retry(exc=e, countdown=120) except Exception as e: print(f"任务执行出错: {str(e)}")
视图中调用异步任务
# views.py from django.shortcuts import render from django.conf import settings import os import uuid from .tasks import transcode_video_task from .models import Video def handle_video_upload(request): if request.method == "POST": video_file = request.FILES["video"] # 生成唯一文件夹名,避免重名冲突 folder = f"uploads/{video_file.name.split('.')[0]}_{uuid.uuid4().hex[:8]}" upload_path = os.path.join(settings.MEDIA_ROOT, folder) os.makedirs(upload_path, exist_ok=True) input_path = os.path.join(upload_path, video_file.name) # 保存用户上传的原始视频 with open(input_path, "wb+") as f: for chunk in video_file.chunks(): f.write(chunk) # 创建主视频记录 rec = Video.objects.create(name=video_file.name, path=os.path.join(folder, video_file.name)) # 提交异步转码任务,立即返回响应 transcode_video_task.delay(rec.id, input_path, folder) return render(request, "upload_success.html", {"video_id": rec.id})
二、修复FFmpeg命令的安全与可靠性问题
- 禁用
shell=True:直接用列表形式传参,既避免shell注入攻击,又能兼容带空格的文件路径 - 明确编码参数:添加
-c:v libx264指定编码器,-crf和-preset平衡画质与性能,-y自动覆盖已存在的输出文件 - 捕获转码状态:用
subprocess.run()代替subprocess.call(),并设置check=True,转码失败时会抛出异常,方便我们做重试或错误处理
三、确保数据库操作仅在转码成功后执行
之前的代码不管FFmpeg是否成功都会执行get_or_create,现在我们把数据库操作放到subprocess.run()成功执行的分支里(try块内,转码命令未抛出异常的情况下),这样只有转码真正完成且成功时,才会往数据库里添加记录。
四、其他优化细节
- 路径规范化:用
settings.MEDIA_ROOT和settings.MEDIA_URL管理媒体路径,不要硬编码media/uploads/ - 任务重试机制:给Celery任务设置重试次数和间隔,避免临时系统资源不足导致的转码失败
- 日志记录:把转码的成功/失败日志写入Django的日志系统,方便后续排查问题
- 重复任务防护:可以给任务添加唯一ID,避免同一个视频被多次提交转码任务
内容的提问来源于stack exchange,提问作者varunj6v1k9
相关产品推荐
相关产品推荐

