如何在FastAPI中巧妙实现延迟队列,让任务乖乖等待?

1.1 消息队列基础与延迟队列概念

消息队列是分布式系统中实现异步通信的核心组件,允许系统解耦、提高吞吐量和可靠性。延迟队列是一种特殊消息队列,可在指定延迟时间后才投递消息,常见于定时任务、失败重试等场景(如订单超时取消)。

延时队列实现原理:

rust 复制代码
生产者 -> [消息队列] -- 延迟时间 --> 消费者
              ↓ 内部排序/时间轮调度

1.2 FastAPI集成消息队列的方案

在FastAPI中实现延迟队列推荐架构:

推荐工具链组合:

  • 消息中间件:Redis Streams(轻量级)或 RabbitMQ(企业级)
  • 任务调度:Celery 或 arq
  • 延时机制:Redis Sorted Set 或 RabbitMQ死信队列

1.3 Redis延迟队列实现方案

以下使用Redis + arq实现全异步延迟队列:

环境配置

bash 复制代码
pip install fastapi==0.109.0 arq==0.26.0 pydantic==2.5.3 redis==5.0.0

项目结构

bash 复制代码
├── main.py          # FastAPI入口
├── worker.py        # ARQ worker
├── schemas.py       # Pydantic数据模型
└── tasks.py         # 延时任务定义

1.3.1 数据模型(schemas.py

python 复制代码
from pydantic import BaseModel

class OrderPayload(BaseModel):
    order_id: str
    expire_minutes: int = 30  # 默认30分钟后过期

class EmailPayload(BaseModel):
    email: str
    template: str = "welcome"

1.3.2 任务定义(tasks.py

python 复制代码
from arq import cron
from .schemas import OrderPayload, EmailPayload

async def process_order(ctx, payload: OrderPayload):
    """订单延迟处理任务"""
    print(f"处理订单 {payload.order_id},已等待 {payload.expire_minutes} 分钟")

async def send_email(ctx, payload: EmailPayload):
    """延迟发送邮件"""
    print(f"发送 {payload.template} 邮件至 {payload.email}")

# ARQ定时任务配置
async def startup(ctx):
    ctx["redis"] = await ctx["pool"]  # 初始化Redis连接

class WorkerSettings:
    cron_jobs = [
        cron(
            process_order, 
            hour={8, 12, 18},  # 每天8/12/18点执行
            run_at_startup=True
        )
    ]
    on_startup = startup

1.3.3 FastAPI主服务(main.py

python 复制代码
from fastapi import FastAPI
from arq.connections import create_pool
from .schemas import OrderPayload, EmailPayload
from .tasks import WorkerSettings

app = FastAPI()

@app.on_event("startup")
async def init_redis():
    """初始化Redis连接池"""
    app.state.redis = await create_pool(RedisSettings())
    
@app.post("/submit-order")
async def submit_order(order: OrderPayload):
    """提交延迟处理的订单"""
    await app.state.redis.enqueue_job(
        "process_order", 
        order.dict(),
        _defer_by=order.expire_minutes * 60  # 转换为秒
    )
    return {"msg": "订单已进入延迟队列"}

@app.post("/welcome-email")
async def schedule_email(email: EmailPayload):
    """延迟3秒发送欢迎邮件"""
    await app.state.redis.enqueue_job(
        "send_email", 
        email.dict(),
        _defer_by=3  # 延迟3秒
    )
    return {"msg": "邮件任务已调度"}

1.3.4 Worker启动(worker.py

bash 复制代码
arq tasks.WorkerSettings

1.4 RabbitMQ实现方案

使用RabbitMQ死信队列(DLX)实现延迟投递:

python 复制代码
# RabbitMQ配置
RABBITMQ_URI = "amqp://user:pass@localhost"
DLX_NAME = "delayed_exchange"

@app.post("/dlx-order")
async def dlx_submit(order: OrderPayload):
    channel = await connect(RABBITMQ_URI)
    
    # 创建带TTL的死信队列
    await channel.queue_declare(
        "order_delay_queue",
        arguments={
            "x-dead-letter-exchange": DLX_NAME,
            "x-message-ttl": order.expire_minutes * 60 * 1000
        }
    )
    
    # 发布延迟消息
    await channel.publish(
        json.dumps(order.dict()).encode(),
        routing_key="order_delay_queue"
    )

1.5 实际应用场景

  1. 电商订单超时:30分钟未支付自动取消订单
  2. 会议提醒:提前15分钟推送通知
  3. 重试机制:API调用失败后延迟重试
  4. 定时报告:每日凌晨生成统计报表

1.6 课后Quiz

问题1 :为什么在延迟队列中需要单独的Worker服务? A) 减少FastAPI主线程负担

B) 实现跨进程任务分发

C) 避免HTTP请求阻塞

D) 以上都是
答案解析 正确答案:D。Worker分离了任务处理逻辑:

  • FastAPI专注HTTP请求响应
  • Worker后台执行耗时/延迟任务
  • Redis/RabbitMQ负责可靠存储

问题2 :RabbitMQ实现延时投递的关键配置是什么?

A) 消息持久化

B) 死信交换器(DLX)

C) 优先队列

D) 路由密钥
答案解析 正确答案:B。实现流程:

  1. 消息进入带TTL的队列
  2. TTL过期后通过x-dead-letter-exchange转发
  3. DLX将消息路由到目标队列

1.7 常见报错解决方案

422 Validation Error

json 复制代码
{
  "detail": [{
    "loc": ["body", "expire_minutes"],
    "msg": "value is not a valid integer",
    "type": "type_error.integer"
  }]
}

解决方案

  1. 检查Pydantic模型字段类型声明
  2. 客户端需发送JSON格式数据:
python 复制代码
# 错误示例(文本格式)
fetch("/submit-order", body="order_id=123") 

# 正确示例
fetch("/submit-order", json={"order_id": "123"})

ConnectionRefusedError

vbnet 复制代码
arq: Redis connection error

解决方案

  1. 确认Redis服务是否启动:redis-cli ping
  2. 检查连接参数:
python 复制代码
# worker.py
class WorkerSettings:
    redis_settings = RedisSettings(host="redis", port=6379)

Task执行异常

javascript 复制代码
Task failed with exception: TypeError('unsupported operand type(s) for...

解决方案

  1. 在任务函数添加try/except:
python 复制代码
async def process_order(ctx, payload):
    try:
        # 业务逻辑
    except Exception as e:
        ctx["log"].error(f"任务失败: {e}")
相关推荐
David爱编程8 分钟前
Java 守护线程 vs 用户线程:一文彻底讲透区别与应用
java·后端
小奏技术25 分钟前
国内APP的隐私进步,从一个“营销授权”弹窗说起
后端·产品
小研说技术43 分钟前
Spring AI存储向量数据
后端
苏三的开发日记43 分钟前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台处于同一台服务器)
后端
苏三的开发日记1 小时前
jenkins部署ruoyi后台记录(jenkins与ruoyi后台不在同一服务器)
后端
陈三一1 小时前
MyBatis OGNL 表达式避坑指南
后端·mybatis
whitepure1 小时前
万字详解JVM
java·jvm·后端
我崽不熬夜1 小时前
Java的条件语句与循环语句:如何高效编写你的程序逻辑?
java·后端·java ee
我崽不熬夜1 小时前
Java中的String、StringBuilder、StringBuffer:究竟该选哪个?
java·后端·java ee
我崽不熬夜2 小时前
Java中的基本数据类型和包装类:你了解它们的区别吗?
java·后端·java ee