Celery 异步任务完全指南:从入门到实战

引言

在开发 Web 应用时,我们经常遇到需要长时间执行的任务:发送邮件、处理大文件、生成报表、训练机器学习模型等。如果这些任务在 HTTP 请求中同步执行,会导致用户等待很久,甚至超时。

Celery 就是解决这个问题的利器------一个强大的分布式任务队列系统。本文将深入浅出地介绍 Celery 的核心概念和实战应用,特别是如何实现文件下载进度实时追踪这个常见需求。

一、Celery 是什么?

1.1 核心架构

Celery 由三个核心组件组成:

scss 复制代码
┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│  Main 程序   │ ───> │ Message Broker│ ───> │Celery Worker │
│ (Flask/Django)│      │   (Redis)    │      │  (执行任务)  │
└──────────────┘      └──────────────┘      └──────────────┘
       │                                            │
       │              ┌──────────────┐             │
       └─────────────>│Result Backend│<────────────┘
                      │   (Redis)    │
                      └──────────────┘
  • Main 程序: 发送任务的客户端(你的 Web 应用)
  • Message Broker: 消息队列,存储待执行的任务(常用 Redis)
  • Celery Worker: 执行任务的工作进程
  • Result Backend: 存储任务结果和状态(可选,常用 Redis)

1.2 快速开始

ini 复制代码
# 安装
pip install celery redis

# 定义任务 (tasks.py)
from celery import Celery

app = Celery('myapp', 
             broker='redis://localhost:6379/0',
             backend='redis://localhost:6379/0')

@app.task
def add(x, y):
    return x + y

# 启动 Worker
# celery -A tasks worker --loglevel=info

# 调用任务 (main.py)
result = add.delay(4, 6)
print(result.get())  # 输出: 10

二、task_id:任务的唯一标识符

2.1 什么是 task_id?

task_id 是每个任务的唯一标识符,类似于快递单号。通过它,Main 程序和 Worker 都能追踪同一个任务。

2.2 生成与传递机制

ini 复制代码
# Main 程序端
result = add.apply_async(args=[10, 20])
task_id = result.id  # Celery 自动生成 UUID
print(f"Task ID: {task_id}")  # 3c8a2c9a-7b5d-4e3f...

关键流程

ini 复制代码
Main 程序                    Redis (Broker)              Worker
    │                             │                        │
    │ ① 生成 task_id="abc-123"    │                        │
    │ ② 发送消息                   │                        │
    │   {id:"abc-123", args:[]}   │                        │
    ├────────────────────────────>│                        │
    │                             │ ③ Worker 拉取消息      │
    │                             ├───────────────────────>│
    │                             │   解析 task_id         │
    │                             │                        │
    │                             │   Redis (Backend)      │
    │ ④ 查询状态                   │         │              │
    │   用 task_id 查询            │         │ ⑤ 写入状态  │
    │<────────────────────────────┼─────────┼──────────────│

重点:task_id 在 Main 程序生成,通过消息传递给 Worker,双方都用它作为 Redis Backend 的键名来读写状态。

2.3 自定义 task_id

ini 复制代码
# 自定义 task_id(业务相关)
custom_id = f"download_{user_id}_{file_id}_{uuid.uuid4().hex[:8]}"

result = download_file.apply_async(
    args=['https://example.com/file.zip', '/tmp/file.zip'],
    task_id=custom_id  # 指定自定义 ID
)

# 后续可以随时查询
from celery.result import AsyncResult
status = AsyncResult(custom_id, app=app)
print(status.state)

好处

  • 便于数据库存储和查询
  • 实现幂等性(避免重复任务)
  • 便于日志追踪和调试

三、AsyncResult:任务结果的代理对象

3.1 什么是 AsyncResult?

AsyncResult 是一个代表任务结果的对象,就像"提货单"------你可以随时用它查询任务状态。

ini 复制代码
# 方式 1: 任务调用时直接获得
result = add.delay(4, 6)
print(type(result))  # <class 'celery.result.AsyncResult'>

# 方式 2: 通过 task_id 重新创建
result = AsyncResult('task-id-from-database', app=app)

3.2 核心属性和方法

bash 复制代码
result = long_task.delay(100)

# 任务标识
print(result.id)          # task_id

# 任务状态
print(result.state)       # PENDING, STARTED, SUCCESS, FAILURE...

# 检查状态
print(result.ready())     # 是否完成 (True/False)
print(result.successful()) # 是否成功
print(result.failed())    # 是否失败

# 获取结果
value = result.get(timeout=10)  # 阻塞等待,最多 10 秒

# 非阻塞获取
if result.ready():
    print(result.result)  # 获取结果或异常

3.3 访问任务元数据

python 复制代码
@app.task(bind=True)
def task_with_progress(self, n):
    for i in range(n):
        self.update_state(
            state='PROGRESS',
            meta={'current': i, 'total': n}
        )
    return 'done'

result = task_with_progress.delay(10)
time.sleep(2)
print(result.info)  # {'current': 5, 'total': 10}

注意:AsyncResult 本身不存储数据,它只是通过 task_id 去 Redis 查询数据的"查询器"。

四、delay() vs apply_async()

4.1 核心区别

ini 复制代码
# delay() - 简化版本
result = task.delay(arg1, arg2, key=value)

# 等价于
result = task.apply_async(
    args=[arg1, arg2], 
    kwargs={'key': value}
)

4.2 功能对比表

功能 delay() apply_async()
传递任务参数
自定义 task_id
延迟执行
定时执行
设置优先级
覆盖队列
任务链

4.3 使用建议

ini 复制代码
# 简单场景 - 使用 delay()
send_email.delay("user@example.com", "Hello")

# 需要追踪或配置 - 使用 apply_async()
result = download_file.apply_async(
    args=['https://example.com/file.zip'],
    task_id=f"download_{user_id}_{timestamp}",
    countdown=60,  # 60 秒后执行
    queue="download",
    priority=9
)

原则 :默认用 delay() 保持简洁,需要高级功能时用 apply_async()

五、bind=True:任务的自我感知

5.1 核心作用

bind=True 让任务函数接收 self 参数,代表任务实例本身,从而可以访问任务信息和控制任务行为。

python 复制代码
# 不使用 bind=True
@app.task
def task1(x, y):
    # ❌ 无法访问 task_id
    # ❌ 无法更新状态
    return x + y

# 使用 bind=True
@app.task(bind=True)
def task2(self, x, y):
    # ✅ 访问 task_id
    print(f"Task ID: {self.request.id}")
    
    # ✅ 更新进度
    self.update_state(
        state='PROGRESS',
        meta={'progress': 50}
    )
    
    return x + y

5.2 三大核心功能

1. 访问任务信息

python 复制代码
@app.task(bind=True)
def inspect_task(self, data):
    print(f"Task ID: {self.request.id}")
    print(f"Task Name: {self.name}")
    print(f"Args: {self.request.args}")
    print(f"Retries: {self.request.retries}")
    print(f"Hostname: {self.request.hostname}")

2. 更新任务状态

ini 复制代码
@app.task(bind=True)
def download_file(self, url, save_path):
    response = requests.get(url, stream=True)
    total = int(response.headers.get('content-length', 0))
    downloaded = 0
    
    for chunk in response.iter_content(8192):
        downloaded += len(chunk)
        
        # 实时更新进度
        self.update_state(
            state='PROGRESS',
            meta={
                'downloaded': downloaded,
                'total': total,
                'percent': int(downloaded / total * 100)
            }
        )

3. 手动重试任务

python 复制代码
@app.task(bind=True, max_retries=3)
def unreliable_api_call(self, url):
    try:
        response = requests.get(url, timeout=5)
        return response.json()
    except requests.RequestException as exc:
        # 指数退避重试
        countdown = 2 ** self.request.retries
        raise self.retry(exc=exc, countdown=countdown)

5.3 使用场景

必须使用 bind=True

  • ✅ 需要实时更新任务进度
  • ✅ 需要记录 task_id 到日志
  • ✅ 需要实现自定义重试逻辑
  • ✅ 文件下载、数据导入、长时间运行的任务

不需要 bind=True

  • ✅ 简单的"发送并忘记"任务
  • ✅ 纯粹的计算任务
  • ✅ 不需要追踪进度的后台任务

六、Queue:任务的分类与隔离

6.1 为什么需要队列?

想象一个场景:你的应用有两种任务

  • 发送邮件:快速,每个任务 1 秒
  • 视频转码:慢速,每个任务 5 分钟

不使用队列隔离

ini 复制代码
单一队列: [邮件1s] [邮件1s] [视频5m] [邮件1s]
                              ↑
                    第3封邮件要等5分钟!

使用队列隔离

less 复制代码
Email队列: [邮件] [邮件] [邮件]  ← Email Worker (快速)
Video队列: [视频] [视频]         ← Video Worker (慢速)

结果: 邮件立即发送,视频慢慢处理,互不影响!

6.2 定义和使用队列

python 复制代码
# 在装饰器中指定队列
@shared_task(queue="email")
def send_email(to, subject, body):
    return f"Email sent to {to}"

@shared_task(queue="video")
def process_video(video_id):
    return f"Video {video_id} processed"

@shared_task(queue="dataset")
def process_dataset(dataset_id):
    return f"Dataset {dataset_id} processed"

# 调用时覆盖队列
result = send_email.apply_async(
    args=["user@example.com", "Hello", "Welcome"],
    queue="high-priority"  # 覆盖默认的 "email" 队列
)

6.3 启动 Worker 监听队列

ini 复制代码
# 只处理 email 队列
celery -A myapp worker -Q email --loglevel=info

# 处理多个队列
celery -A myapp worker -Q email,video,dataset --loglevel=info

# 多个 Worker 分别处理不同队列
# Terminal 1: 快速任务
celery -A myapp worker -Q email,api -n fast_worker

# Terminal 2: 慢速任务
celery -A myapp worker -Q video,dataset -n slow_worker

6.4 队列设计原则

按任务类型分类

less 复制代码
@shared_task(queue="email")    # 邮件任务
@shared_task(queue="sms")      # 短信任务
@shared_task(queue="report")   # 报表任务

按执行速度分类

less 复制代码
@shared_task(queue="fast")     # < 1秒
@shared_task(queue="normal")   # 1-10秒
@shared_task(queue="slow")     # > 10秒

按优先级分类

less 复制代码
@shared_task(queue="high-priority")
@shared_task(queue="normal")
@shared_task(queue="low-priority")

按资源需求分类

less 复制代码
@shared_task(queue="cpu-intensive")  # CPU 密集型
@shared_task(queue="io-intensive")   # IO 密集型
@shared_task(queue="memory-heavy")   # 内存密集型

6.5 生产环境部署架构

css 复制代码
# 服务器 1 (8核 CPU) - 快速任务
celery -A myapp worker -Q high-priority,api,email -c 4 -n server1

# 服务器 2 (16核 CPU) - 重型任务
celery -A myapp worker -Q video,dataset,ml -c 8 -n server2

# 服务器 3 (4核 CPU) - 普通任务
celery -A myapp worker -Q default,background -c 4 -n server3

参数说明

  • -Q: 监听的队列列表
  • -c: 并发数(Worker 进程数)
  • -n: Worker 名称

七、实战案例:文件下载进度实时追踪

现在我们把所有知识点整合,实现一个完整的文件下载进度追踪系统。

7.1 Celery 任务定义

python 复制代码
# celery_app.py
from celery import Celery
import requests

app = Celery(
    'tasks',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/0'
)

@app.task(bind=True, queue="download")
def download_file(self, url, save_path):
    """
    下载文件任务,实时更新进度
    
    关键点:
    1. bind=True - 获取 self 来更新状态
    2. queue="download" - 使用专门的下载队列
    """
    try:
        response = requests.get(url, stream=True)
        total_size = int(response.headers.get('content-length', 0))
        downloaded_size = 0
        
        with open(save_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
                    downloaded_size += len(chunk)
                    
                    # 计算进度百分比
                    progress = int((downloaded_size / total_size) * 100) if total_size > 0 else 0
                    
                    # 关键:更新任务状态和进度信息
                    self.update_state(
                        state='PROGRESS',
                        meta={
                            'current': downloaded_size,
                            'total': total_size,
                            'progress': progress,
                            'status': f'Downloading... {progress}%'
                        }
                    )
        
        # 任务完成
        return {
            'current': total_size,
            'total': total_size,
            'progress': 100,
            'status': 'Download completed',
            'file_path': save_path
        }
        
    except Exception as e:
        # 任务失败
        self.update_state(
            state='FAILURE',
            meta={
                'status': 'Download failed',
                'error': str(e)
            }
        )
        raise

7.2 Flask API 接口

python 复制代码
# flask_server.py
from flask import Flask, jsonify, request
from celery.result import AsyncResult
from celery_app import download_file, app as celery_app
import uuid

app = Flask(__name__)

@app.route('/start_download', methods=['POST'])
def start_download():
    """
    启动下载任务
    
    请求体:
    {
        "url": "https://example.com/file.zip",
        "save_path": "/tmp/file.zip",
        "user_id": "user_123"
    }
    """
    data = request.json
    url = data.get('url')
    save_path = data.get('save_path')
    user_id = data.get('user_id')
    
    if not url or not save_path:
        return jsonify({'error': 'url and save_path are required'}), 400
    
    # 生成业务相关的 task_id
    task_id = f"download_{user_id}_{uuid.uuid4().hex[:8]}"
    
    # 启动异步任务,指定自定义 task_id
    task = download_file.apply_async(
        args=[url, save_path],
        task_id=task_id,
        queue="download"  # 可以覆盖装饰器中的队列
    )
    
    # 可以将 task_id 保存到数据库
    # save_to_database(user_id, task_id, url)
    
    return jsonify({
        'task_id': task.id,
        'status': 'Task started',
        'progress_url': f'/download_progress/{task.id}'
    }), 202


@app.route('/download_progress/<task_id>', methods=['GET'])
def get_download_progress(task_id):
    """
    获取下载进度
    
    返回示例:
    {
        "state": "PROGRESS",
        "current": 1024000,
        "total": 10240000,
        "progress": 10,
        "status": "Downloading... 10%"
    }
    """
    # 通过 task_id 创建 AsyncResult 对象
    task = AsyncResult(task_id, app=celery_app)
    
    if task.state == 'PENDING':
        response = {
            'state': task.state,
            'current': 0,
            'total': 1,
            'progress': 0,
            'status': 'Task is waiting...'
        }
    elif task.state == 'PROGRESS':
        response = {
            'state': task.state,
            'current': task.info.get('current', 0),
            'total': task.info.get('total', 1),
            'progress': task.info.get('progress', 0),
            'status': task.info.get('status', '')
        }
    elif task.state == 'SUCCESS':
        response = {
            'state': task.state,
            'current': task.info.get('current', 1),
            'total': task.info.get('total', 1),
            'progress': 100,
            'status': task.info.get('status', 'Completed'),
            'result': task.info
        }
    elif task.state == 'FAILURE':
        response = {
            'state': task.state,
            'current': 0,
            'total': 1,
            'progress': 0,
            'status': str(task.info),
            'error': str(task.info)
        }
    else:
        response = {
            'state': task.state,
            'status': 'Unknown state'
        }
    
    return jsonify(response)


@app.route('/cancel_download/<task_id>', methods=['POST'])
def cancel_download(task_id):
    """取消下载任务"""
    task = AsyncResult(task_id, app=celery_app)
    task.revoke(terminate=True)
    
    return jsonify({
        'status': 'Task cancelled',
        'task_id': task_id
    })


if __name__ == '__main__':
    app.run(debug=True, port=5000)

7.3 前端调用示例

javascript 复制代码
// 启动下载
fetch('http://localhost:5000/start_download', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
        url: 'https://example.com/large-file.zip',
        save_path: '/tmp/downloaded-file.zip',
        user_id: 'user_123'
    })
})
.then(res => res.json())
.then(data => {
    const taskId = data.task_id;
    
    // 轮询获取进度
    const interval = setInterval(() => {
        fetch(`http://localhost:5000/download_progress/${taskId}`)
            .then(res => res.json())
            .then(progress => {
                console.log(`Progress: ${progress.progress}%`);
                console.log(`Status: ${progress.status}`);
                
                // 更新进度条
                document.getElementById('progress-bar').style.width = 
                    `${progress.progress}%`;
                
                // 任务完成或失败,停止轮询
                if (progress.state === 'SUCCESS' || 
                    progress.state === 'FAILURE') {
                    clearInterval(interval);
                    console.log('Download finished!');
                }
            });
    }, 1000);  // 每秒查询一次
});

7.4 部署步骤

makefile 复制代码
# 1. 安装依赖
pip install celery redis flask requests

# 2. 启动 Redis
redis-server

# 3. 启动 Celery Worker(专门处理下载队列)
celery -A celery_app worker -Q download --loglevel=info

# 4. 启动 Flask 服务器
python flask_server.py

# 5. 测试
curl -X POST http://localhost:5000/start_download \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/file.zip","save_path":"/tmp/file.zip","user_id":"user_123"}'

八、核心概念总结

概念 作用 何时使用
task_id 任务唯一标识符 需要追踪任务状态时
AsyncResult 任务结果代理对象 查询任务状态和结果
delay() 简化的任务调用 简单场景,不需要配置
apply_async() 完整的任务调用 需要 task_id、延迟、优先级等
bind=True 任务自我感知 需要更新进度、重试、访问元数据
queue 任务分类隔离 不同类型任务需要独立处理

九、最佳实践建议

9.1 任务设计

  1. 粒度适中:任务不要太大(避免超时)也不要太小(避免开销)
  2. 幂等性:任务应该可以安全重试,多次执行结果相同
  3. 错误处理:使用 try-except 捕获异常,避免任务崩溃
  4. 超时设置:为长时间任务设置合理的超时时间
python 复制代码
@app.task(bind=True, time_limit=3600, soft_time_limit=3000)
def long_task(self, data):
    # time_limit: 硬超时(强制终止)
    # soft_time_limit: 软超时(抛出异常)
    pass

9.2 队列设计

  1. 按业务分类:email, sms, report, video, dataset
  2. 按优先级分类:high, normal, low
  3. 按资源需求分类:cpu-intensive, io-intensive
  4. 合理配置 Worker:快速任务多并发,慢速任务少并发

9.3 监控和调试

shell 复制代码
# 使用 Flower 监控工具
pip install flower
celery -A myapp flower

# 访问 http://localhost:5555 查看:
# - 各队列的任务数量
# - Worker 的运行状态
# - 任务执行历史
# - 实时监控图表

9.4 生产环境配置

ini 复制代码
# celery_config.py
from kombu import Queue

# 任务路由
app.conf.task_routes = {
    'myapp.tasks.send_email': {'queue': 'email'},
    'myapp.tasks.process_video': {'queue': 'video'},
}

# 结果过期时间
app.conf.result_expires = 3600  # 1小时

# 任务序列化
app.conf.task_serializer = 'json'
app.conf.result_serializer = 'json'
app.conf.accept_content = ['json']

# 时区设置
app.conf.timezone = 'Asia/Shanghai'
app.conf.enable_utc = True

# Worker 预取设置
app.conf.worker_prefetch_multiplier = 4

# 任务确认
app.conf.task_acks_late = True

十、常见问题解答

Q1: 任务执行失败了怎么办?

python 复制代码
@app.task(bind=True, max_retries=3, default_retry_delay=60)
def retry_task(self, data):
    try:
        # 可能失败的操作
        risky_operation(data)
    except Exception as exc:
        # 自动重试
        raise self.retry(exc=exc)

Q2: 如何避免重复任务?

ini 复制代码
# 使用固定的 task_id
task_id = f"process_user_{user_id}"
result = process_user.apply_async(
    args=[user_id],
    task_id=task_id
)
# 如果 task_id 已存在,Celery 会拒绝或忽略

Q3: 如何实现定时任务?

python 复制代码
from celery.schedules import crontab

# 配置定时任务
app.conf.beat_schedule = {
    'cleanup-every-day': {
        'task': 'tasks.cleanup',
        'schedule': crontab(hour=3, minute=0),  # 每天凌晨3点
    },
}

# 启动 Beat 调度器
# celery -A myapp beat --loglevel=info

Q4: Worker 数量如何设置?

r 复制代码
# CPU 密集型:Worker 数 = CPU 核心数
celery -A myapp worker -c 4

# IO 密集型:Worker 数 = CPU 核心数 * 2-4
celery -A myapp worker -c 16

# 混合型:根据实际负载调整

结语

Celery 是 Python 生态中最成熟的异步任务队列系统,掌握它的核心概念------task_id、AsyncResult、bind、queue------能让你轻松应对各种异步任务场景。

参考资源

相关推荐
MediaTea2 小时前
Python 装饰器:@property_name.deleter
开发语言·python
Cherry的跨界思维2 小时前
5、Python长图拼接终极指南:Pillow/OpenCV/ImageMagick三方案
javascript·python·opencv·webpack·django·pillow·pygame
acethanlic3 小时前
使用Ruff进行Python代码Format、lint和fix
python
codists3 小时前
在 Pycharm 中 debug Scrapy 项目
python
Pyeako3 小时前
操作HTML网页(PyCharm版)
爬虫·python·html
清静诗意3 小时前
Python 异步编程与 Gevent 实战指南
python·协程·gevent
linzeyang3 小时前
Advent of Code 2025 挑战全手写代码 Day 8 - 游乐场
后端·python
超级种码3 小时前
JVM 字节码指令活用手册(基于 Java 17 SE 规范)
java·jvm·python
子午3 小时前
【垃圾识别系统】Python+TensorFlow+Django+人工智能+深度学习+卷积神经网络算法
人工智能·python·深度学习