一、FastApi异步任务Demo应用构建:Celery+RabbitMQ+Redis
1.环境准备
1.项目依赖包安装:
pip install celery fastapi asyncpg sqlalchemy pydantic redis
windows本地安装RabbitMQ需要先安装Erlang环境,详情可参考博客:RabbitMQ使用教程(超详细)-CSDN博客,包括详细安装教程。
2.本地启动RabbitMQ:
进入下面命令行
开启web管理端
进入管理端,使用默认账号 guest 密码 guest 登录进去
首次登录后,建议在创建一个新账号,并给于相应权限。
创建新的virtual host,并指定其所属用户下,关于RabbitMQ中的virtual host,应该使不同的业务线绑定不同的virtual host。(RabbiMQ和其中不同的virtual host可类比于mysql和mysql中的不同业务数据库)
3.本地启动redis服务,安装redis后,直接双击启动redis-server.exe
本地启动完成状态:
2.配置环境变量
需配置连接RabbitMQ和redis的地址
# RabbitMQ 配置
RABBITMQ_URL=amqp://admin:admin@localhost:5672/fastapi
# Redis 配置
REDIS_URL=redis://localhost:6379/0
# Celery 配置
CELERY_RESULT_EXPIRES=3600 # 结果保留1小时,celery任务结果后端配置在redis中,即任务在redis中的有效时间
2.1 Broker(消息代理)URL
(1)RabbitMQ 标准格式
amqp://{user}:{password}@{host}:{port}/{vhost}?param=value
参数 | 说明 | 默认值/示例 |
---|---|---|
user | RabbitMQ 用户名 | guest |
password | RabbitMQ 密码 | guest |
host | RabbitMQ 服务器地址 | localhost |
port | 服务端口 | 5672 |
vhost | 虚拟主机(类似命名空间) | / (需 URL 编码为 %2F ) |
ssl | 启用 SSL 加密 | amqps://... |
示例:
# 不带SSL本地开发环境配置
broker_url = "amqp://admin:admin@localhost:5672/fastapi"
# 带 SSL 的生产环境配置
broker_url = "amqps://prod_user:SecurePass123@rabbitmq.prod.com:5671/prod_vhost"
2.2 Backend(结果后端)URL
(1)Redis 标准格式
redis://:{password}@{host}:{port}/{db}?param=value
参数 | 说明 | 默认值/示例 |
---|---|---|
password | Redis 密码(若无密码可省略 : ) | 无 |
host | Redis 服务器地址 | localhost |
port | 服务端口 | 6379 |
db | 数据库编号(0-15) | 0 |
ssl | 启用 SSL 加密 | rediss://... |
ssl_cert_reqs | SSL 证书验证级别(none /optional /required ) | required (生产推荐) |
socket_timeout | 连接超时时间(秒) | 30 |
retry_on_timeout | 超时后重试 | True |
示例:
# 不带 SSL 本地开发环境配置
backend_url = "redis://localhost:6379/0"
# 带 SSL 和超时设置的配置
backend_url = "rediss://:RedisPass123@redis.prod.com:6379/0?ssl_cert_reqs=required&socket_timeout=10"
3.Celery实例配置与创建
示例:
from celery import Celery
from dotenv import load_dotenv
import os
# 加载环境变量
load_dotenv()
# 创建celery实例
celery_app = Celery(
"tasks",
# 消息代理(负责传递任务消息的中间件),这里使用RabbitMQ
broker=os.getenv("RABBITMQ_URL"),
# 结果后端(存储任务执行结果,包括状态、返回值,供查询使用),这里使用redis
backend=os.getenv("REDIS_URL"),
# 统一序列化方式(保证任务参数跨语言兼容性)
task_serializer="json",
result_serializer="json",
# 防止安全漏洞,防止代码注入攻击(仅接受可信格式)
accept_content=["json"],
# 时区设置
timezone="Asia/Shanghai",
enable_utc=True # 保证分布式系统时间一致性
)
# 配置任务结果过期时间
celery_app.conf.result_expires = int(os.getenv("CELERY_RESULT_EXPIRES", 3600))
4.Celery任务创建
最佳实践建议任务模块化:将任务定义放在单独文件(如 tasks.py
),具体的Celery任务函数也即对应某个具体的业务逻辑
示例:
from celery_app.celery_app import celery_app
import time
# 创建celery任务(对应与具体的处理业务)
@celery_app.task(
name="resume_optimize", # 自定义任务名称
bind=True, # 允许访问任务上下文(self 参数)
max_retries=3, # 最大重试次数
soft_time_limit=60 # 超时时间60秒
)
def resume_optimize(self, resume_id: int):
for i in range(10):
time.sleep(1) # 模拟简历分析任务 10秒
print("简历id("+str(resume_id)+")正在进行优化...")
self.update_state(
state='progress',
meta={
'msg': "简历正在优化中"
}
)
# 响应任务结果
return "简历优化结果..."
5.FastAPI 主程序
在FastAPI主程序中导入celery实例,提供一个post接口创建简历优化task异步执行,同时提供一个get接口查询对应优化task任务状态,完成后响应优化结果。
示例:
from fastapi import FastAPI
from celery_app.celery_app import celery_app
from celery.result import AsyncResult
from dotenv import load_dotenv
from ResponseUtil import ResponseUtil
app = FastAPI(title="Async Task Demo")
load_dotenv()
@app.post("/resume/optimize/{resume_id}")
async def optimize_resume(resume_id: int):
"""优化指定id简历"""
try:
# 1.修改数据库中简历状态为正在优化(或者直接在任务中修改更好?)
# 2.发送携带参数到指定celery任务(异步处理)
task = celery_app.send_task("resume_optimize", args=[resume_id])
# 3.返回前端当前优化任务id
return {
"code": 202,
"msg": "简历优化任务已开启",
"data": {
"optimize_task_id": task.id
}
}
except Exception as e:
print("优化简历失败")
return {
"code": 500,
"msg": "服务端异常,简历优化失败,请稍后再试",
"data": None
}
@app.get("/resume/optimize/task/{task_id}")
async def get_optimize_task_status(task_id: str):
# 1.查询任务状态(通过AsyncResult)
result = AsyncResult(task_id, app=celery_app)
# 2.判断任务状态
if result.state == "SUCCESS": # 等价与 if result.successful():
# 2.1 任务完成(返回优化完成的结果)
return ResponseUtil.success(
msg="优化任务已完成",
data=result.result # celery任务返回结果
)
elif result.state == "FAILURE":
# 2.2 任务失败
return ResponseUtil.error(
msg="优化任务失败,请稍后再重试"
)
# 2.3 其他情况都认为任务正在进行
return ResponseUtil.progress()
6.运行
保证环境准备时启用了RabbitMQ和Redis。
启动 FastAPI
uvicorn main:app --host 127.0.0.1 --port 8000 --reload
启动 Celery Worker
celery -A celery_app worker --pool=solo --loglevel=info
注意:启用Celery Worker时,需要保证正确启动我们之前创建的那个Celery实例的worker,需要cd到指定目录下面,并且-A参数后面携带的参数需要与Celery实例名称一致。
启动成功示例:
7.接口测试
通过创建task接口创建优化任务:
通过查询任务状态查询task,task正在进行:
task任务进行中celery后台日志:
task完成状态:
8.验证Redis存储的task
打开redis-cli.exe
redis客户端,使用以下命令看到所有还没有过期的task(有效期1个小时)
KEYS "celery-task-meta-*"
# 具体的某个task任务是否存在
keys "celery-task-meta-38105081-5904-4bcc-81e0-861387008957"
9.使用Flower监控面板监控task
安装依赖:
pip install Flower
安装成功后,先开一个终端启动Celery worke:
celery -A app_celery.celery_app worker --pool=solo --loglevel=info
然后再开一个终端:
celery -A app_celery.celery_app flower --port=5555 # port指定端口
浏览器打开访问 http://localhost:5555 即可访问,如下图所示:
可以点击看到每个task的具体内容:
10. 开启终端服务与应用数量
在本地开发时,需要启动5个终端服务/应用,包括
- 本地Redis 服务端(作为 task 的结果后端,存储存储任务执行结果,包括状态、返回值,供查询使用)
- 本地RabbitMQ服务(作为task的消息代理,负责传递任务消息的中间件)
- 启动Celery worker
- 通过uvicorn 启动 fastapi应用
- 启动Flower 监控 celery
二、踩坑与解决
1.celery worker启动时-A参数值问题
项目结构如下
.(F:\code\fastApiProject\ )
├── app_celery # celery相关模块
│ ├── __init__.py
│ ├── celery_app.py # Celery 实例模块(包括一个Celery()实例,命名为celery_app)
│ └── tasks.py # 任务定义
|
├── main.py # FastAPI 主程序
如果在F:\code\fastApiProject>
下启动,则使用以下命令
celery -A app_celery.celery_app.celery_app worker --pool=solo --loglevel=info
如果在F:\code\fastApiProject\app_celery>
下启动,使用:
celery -A celery_app.celery_app worker --pool=solo --loglevel=info
如果celery_app.py模块中,创建的Celery()实例名称与所在模块相同,为celery_app,则可以省略具体的实例名称,如在F:\code\fastApiProject\app_celery>
下启动:
celery -A celery_app worker --pool=solo --loglevel=info
但如果Celery()实例名称与其所在模块名不同,则启动worker的时候必须定位到创建的那个具体实例!
2.celery worker 启动–pool=solo参数
启动–pool=solo参数不能缺少,会报错:
ERROR/MainProcess] Process 'SpawnPoolWorker-19' pid:25876 exited with 'exitcode 1'
虽然报错后好像启动成功了,但是发现RabbitMQ中消息都是unacked状态,即worker没有完成消费并确认,所创建的task一直处在正在进行状态。
3.Celery启动后Celery task缺失
celery_app = Celery(
"celery_app",
# 消息代理(负责传递任务消息的中间件),这里使用RabbitMQ
broker=os.getenv("RABBITMQ_URL"),
# 结果后端(存储任务执行结果,包括状态、返回值,供查询使用),这里使用redis
backend=os.getenv("REDIS_URL"),
task_serializer="json",
result_serializer="json",
accept_content=["json"],
timezone="Asia/Shanghai",
enable_utc=True,
# include=["app_celery.tasks"] # 显示导入多个任务模块
)
由于在创建Celery()实例时,没有include对应tasks模块(tasks模块与创建Celery()实例模块分开的),会导致Celery中根本就没有对应task。
先说一下,创建Celery()实例时第一个参数。
Celery()构造函数的第一个参数(main参数)的含义:
这个参数是Celery应用的名称(name),它有以下几个重要作用:
-
应用标识:
-
这是Celery应用的唯一标识符
-
用于在分布式系统中识别不同的Celery应用
-
在日志和监控中显示的应用名称
-
-
任务命名空间:
- 它决定了任务名称的前缀
- 例如:如果设置main=“myapp”,那么任务名称会变成myapp.tasks.task_name
- 它决定了任务名称的前缀
-
模块导入路径:
-
它告诉Celery从哪里导入任务模块
-
如果设置为"celery_app",Celery会尝试从celery_app模块导入任务
-
上面第三点说Celery会尝试从celery_app模块导入任务,但是我将其所在模块命名为celery_app之后,尝试多次之后,还是没有成功导入celery_app同一个模块下的tasks.py中的任务。于是显示使用include进行导入。并且在实际项目中也应该显式使用include导入对应task模块更好,更加清晰,特别是当tasks模块也有多个时。
成功导入:
同时,在启动时celery worker时,成功导入的task也会显示:
三、Celery任务状态
在 Celery 中,任务状态(state
)的更新机制分为 系统自动管理 和 开发者主动设置 两种方式。以下是完整的解释:
1.Celery的预定义状态(系统自动管理)
状态值 | 触发条件 | 典型场景 |
---|---|---|
PENDING | 任务已发送到 Broker,但尚未被 Worker 接收 | 任务刚创建时的初始状态 |
RECEIVED | Worker 已接收到任务 | 任务进入执行队列 |
STARTED | Worker 开始执行任务(需配置 task_track_started=True ) | 需要跟踪任务启动时间的场景 |
SUCCESS | 任务成功完成,并返回结果 | 函数正常执行且未抛出异常 |
FAILURE | 任务执行过程中抛出未捕获的异常 | 代码存在 Bug 或外部依赖故障 |
RETRY | 任务进入重试流程(通过 self.retry() 触发) | 配置了自动重试策略的任务 |
示例:系统自动管理状态
@celery.task
def add(x, y):
return x + y # 成功时自动设为 SUCCESS
@celery.task
def buggy_task():
raise ValueError("Oops!") # 失败时自动设为 FAILURE
2.开发者自定义状态(主动设置)
通过 self.update_state()
方法可以自定义状态和元数据:
@celery.task(bind=True)
def progress_task(self, total_steps):
for i in range(total_steps):
time.sleep(1)
self.update_state(
state="PROGRESS", # 自定义状态名
meta={"current": i+1, "total": total_steps} # 附加元数据
)
return "Done"
状态查询结果示例:
{
"task_id": "550e8400...",
"status": "PROGRESS",
"result": {
"current": 3,
"total": 10
}
}
3.任务状态部分判断方法
方法 | 返回值 | 等价条件 |
---|---|---|
task.ready() | bool | state in (SUCCESS, FAILURE) |
task.failed() | bool | state == FAILURE |
task.successful() | bool | state == SUCCESS |
四、Celery task任务名称规则
在 Celery 中,当你使用 @celery_app.task
装饰器定义任务 且没有显式指定 name
参数 时,Celery 默认会按照以下规则生成任务名称:
1、默认任务名称规则
@celery_app.task # 不指定 name
def my_task():
pass
# 默认任务名称 = 模块路径 + 函数名
# 例如:如果此任务定义在 `tasks.py` 中,则名称为 `tasks.my_task`
2、验证默认名称
通过以下方式查看实际注册的任务名称:
celery -A your_app inspect registered
输出示例:
-> worker@host: OK
* tasks.my_task # 默认生成的任务名
2、send_task
调用规则
场景 | 正确调用方式 | 错误调用方式 |
---|---|---|
默认任务名 | send_task("tasks.my_task") | send_task("my_task") |
显式命名 | send_task("custom_name") | send_task("tasks.custom_name") |
五、Celery消息代理和结果后端选择
在 Celery 中,推荐 Redis 作为结果后端、RabbitMQ 作为消息代理 的组合,主要基于两者的设计特性和适用场景。以下是详细分析:
1、消息代理(Broker)的选择:为什么用 RabbitMQ?
1.1 核心优势
- 协议支持:RabbitMQ 是 AMQP 协议 的标准实现,天生为消息队列设计,支持复杂路由、消息确认、持久化等企业级特性。
- 可靠性:
- 消息持久化(磁盘存储)
- 投递确认机制(Publisher Confirms)
- 死信队列(Dead Letter Exchanges)
- 吞吐量:在高并发场景下,RabbitMQ 的消息处理能力(尤其是顺序性和可靠性)显著优于 Redis。
1.2 适用场景
- 任务分发:需要确保消息不丢失(如订单处理、支付回调)
- 复杂路由:通过 Exchange 实现 Topic、Fanout 等路由模式
- 优先级队列:支持不同优先级的任务分级处理
1.3 性能对比
场景 | RabbitMQ | Redis |
---|---|---|
10万级消息/秒 | ✅ 稳定处理 | ⚠️ 可能阻塞 |
消息持久化 | ✅ 原生支持 | ❌ 依赖配置 |
复杂路由 | ✅ 灵活 | ❌ 简单队列 |
2、结果后端(Result Backend)的选择:为什么用 Redis?
2.1 核心优势
- 读写性能:Redis 是内存数据库,查询任务状态的速度比 RabbitMQ 快 10 倍以上。
- 数据结构灵活:
- 支持 Hash、SortedSet 等结构,方便存储复杂任务结果
- 天然适合存储键值对形式的任务元数据
- 过期策略:可自动清理过期结果(通过
result_expires
配置),避免存储膨胀。
2.2 适用场景
- 高频状态查询:前端需要实时轮询任务进度
- 临时数据存储:任务结果无需永久保存
- 高并发访问:支持数千并发连接查询任务状态
2.3 性能对比
操作 | Redis (ops/sec) | RabbitMQ (ops/sec) |
---|---|---|
写入任务结果 | 100,000+ | 5,000-10,000 |
读取任务状态 | 50,000+ | 1,000-2,000 |
3、为什么不统一使用 RabbitMQ?
虽然 RabbitMQ 也可作为结果后端,但在实践中存在以下问题:
- 查询效率低:
- RabbitMQ 需要维护额外的队列存储结果,每次查询需遍历队列,时间复杂度为 O(n)
- Redis 通过 Key-Value 直接定位结果,时间复杂度为 O(1)
- 功能缺失:
- RabbitMQ 没有原生过期机制,需手动清理结果
- Redis 可通过
EXPIRE
自动管理结果生命周期
- 存储成本高:
- RabbitMQ 的磁盘持久化对结果存储是过度设计
- Redis 内存存储更经济(结果数据通常临时性强)
4、生产环境推荐架构
关键配置
# Celery 配置示例
app = Celery(
"proj",
broker="amqp://user:pass@rabbitmq-host:5672/proj_vhost",
backend="redis://:password@redis-host:6379/0",
result_extended=True, # 启用详细结果存储
result_expires=86400 # 结果保存24小时
)
5、何时选择其他组合?
场景 | 推荐组合 | 原因 |
---|---|---|
简单原型开发 | Redis 同时作为 Broker 和 Backend | 快速搭建,减少依赖 |
极高吞吐量任务(无需持久化) | RabbitMQ + Memcached | Memcached 比 Redis 的读性能更高 |
企业级审计需求 | RabbitMQ + PostgreSQL | 需要永久保存任务日志和结果 |
总结
- RabbitMQ:专注 可靠的消息传递,解决任务分发中的顺序性、持久化和复杂路由问题。
- Redis:专注 高效的状态查询,解决结果数据的快速读写和生命周期管理问题。
两者的组合实现了 消息处理 和 状态管理 的解耦,是性能和可靠性权衡后的最佳实践。