FastApi应用长时间异步任务处理:Celery+RabbitMQ+Redis

一、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
参数说明默认值/示例
userRabbitMQ 用户名guest
passwordRabbitMQ 密码guest
hostRabbitMQ 服务器地址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
参数说明默认值/示例
passwordRedis 密码(若无密码可省略 :
hostRedis 服务器地址localhost
port服务端口6379
db数据库编号(0-15)0
ssl启用 SSL 加密rediss://...
ssl_cert_reqsSSL 证书验证级别(none/optional/requiredrequired(生产推荐)
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),它有以下几个重要作用:

  1. 应用标识:

    • 这是Celery应用的唯一标识符

    • 用于在分布式系统中识别不同的Celery应用

    • 在日志和监控中显示的应用名称

  2. 任务命名空间:

    • 它决定了任务名称的前缀
      • 例如:如果设置main=“myapp”,那么任务名称会变成myapp.tasks.task_name
  3. 模块导入路径:

    • 它告诉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 接收任务刚创建时的初始状态
RECEIVEDWorker 已接收到任务任务进入执行队列
STARTEDWorker 开始执行任务(需配置 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()boolstate in (SUCCESS, FAILURE)
task.failed()boolstate == FAILURE
task.successful()boolstate == 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 性能对比
场景RabbitMQRedis
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 也可作为结果后端,但在实践中存在以下问题:

  1. 查询效率低
    • RabbitMQ 需要维护额外的队列存储结果,每次查询需遍历队列,时间复杂度为 O(n)
    • Redis 通过 Key-Value 直接定位结果,时间复杂度为 O(1)
  2. 功能缺失
    • RabbitMQ 没有原生过期机制,需手动清理结果
    • Redis 可通过 EXPIRE 自动管理结果生命周期
  3. 存储成本高
    • RabbitMQ 的磁盘持久化对结果存储是过度设计
    • Redis 内存存储更经济(结果数据通常临时性强)

4、生产环境推荐架构

发送任务
拉取任务
写入结果
查询状态
客户端
RabbitMQ Broker
Celery Worker
Redis Backend
关键配置
# 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 + MemcachedMemcached 比 Redis 的读性能更高
企业级审计需求RabbitMQ + PostgreSQL需要永久保存任务日志和结果

总结

  • RabbitMQ:专注 可靠的消息传递,解决任务分发中的顺序性、持久化和复杂路由问题。
  • Redis:专注 高效的状态查询,解决结果数据的快速读写和生命周期管理问题。

两者的组合实现了 消息处理状态管理 的解耦,是性能和可靠性权衡后的最佳实践。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值