FastAPI 的 BackgroundTasks 有什么用?和 Celery 这类任务队列有什么区别?
简化版
FastAPI 的 BackgroundTasks 用来在响应返回后执行一些轻量后台动作,比如写日志、发通知、清理临时文件。它不是可靠任务队列:进程重启会丢任务,也没有跨机器调度、持久化、重试和监控;耗时、关键、需要重试的任务应交给 Celery、RQ、Dramatiq 等任务队列。
详细版
BackgroundTasks 的好处是简单,不需要额外组件。接口里把函数和参数加入后台任务,FastAPI/Starlette 会在响应发送后执行它。它适合“失败影响不大、耗时短、和当前进程同生命周期”的工作。
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def send_email(to: str):
...
@app.post("/register")
def register(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, email)
return {"ok": True}
Celery 这类任务队列则有 broker、worker、结果存储、重试、定时任务、监控等能力,适合发邮件、生成报表、视频转码、调用外部系统等可靠异步任务。面试回答关键是区分“响应后执行的小尾巴”和“可持久化、可重试、可观测的分布式任务”。
完整版教学
一、为什么 Web 接口需要后台任务
HTTP 接口最好尽快返回。用户注册接口如果同步发邮件、同步写外部 CRM、同步生成欢迎礼包,响应时间会被最慢的外部依赖拖住。后台任务的目的就是把不必阻塞响应的动作移到响应之后。
假设注册本身写数据库需要 40ms,发邮件需要 800ms。如果同步发邮件,用户看到响应至少 840ms;如果把发邮件放到后台,接口可能 50ms 左右返回,邮件稍后发送。这个体验差异在高并发下会非常明显。
同步:
请求 -> 写库 40ms -> 发邮件 800ms -> 返回
后台:
请求 -> 写库 40ms -> 返回
-> 响应后发邮件
但“后台”不自动等于“可靠”。把动作挪到响应之后只是降低用户等待,不代表任务一定能执行成功。
二、BackgroundTasks 的执行模型
FastAPI 的 BackgroundTasks 来自 Starlette。你在请求处理函数中注入它,调用 add_task() 登记函数和参数。响应发送完成后,同一应用进程会执行这些任务。
def write_audit_log(user_id: int, action: str):
with open("audit.log", "a", encoding="utf-8") as f:
f.write(f"{user_id}:{action}\n")
@app.post("/login")
def login(user_id: int, background_tasks: BackgroundTasks):
background_tasks.add_task(write_audit_log, user_id, "login")
return {"status": "ok"}
它的轻量来自“没有 broker、没有 worker、没有持久化”。这也正是它的边界:如果 Uvicorn worker 在响应后、任务执行前崩溃,任务可能直接丢失;如果任务执行很慢,也仍然消耗当前服务进程资源。
三、BackgroundTasks 适合什么场景
适合的任务通常满足四个条件:执行短、失败影响小、不需要复杂重试、不要求跨服务调度。比如写访问日志、删除临时文件、发送非关键埋点、触发本地缓存刷新。
| 场景 | 是否适合 BackgroundTasks | 原因 |
|---|---|---|
| 删除临时上传文件 | 适合 | 短任务,失败可下次清理 |
| 写非关键审计日志 | 视情况适合 | 可接受少量失败时可以 |
| 发送支付成功消息 | 不适合 | 关键事件,需要可靠投递 |
| 生成 5 分钟报表 | 不适合 | 耗时长,会占服务进程 |
| 视频转码 | 不适合 | CPU/IO 重,需独立 worker |
判断标准很朴素:这个任务丢了会不会造成业务事故?会的话就不要只放在 BackgroundTasks。
四、Celery 这类任务队列多了什么
Celery 的核心结构是 Web 进程把任务消息发到 broker,worker 从 broker 拉任务执行。broker 可以是 Redis、RabbitMQ 等,任务可以重试、延迟执行、定时调度,并通过监控工具观察状态。
FastAPI
|
| delay(order_id)
v
Broker(Redis/RabbitMQ)
|
v
Celery Worker -> 执行业务任务 -> 重试/记录结果
数字化对比:1000 个报表生成任务,每个 30 秒。如果放在 Web 进程里,接口服务会被拖垮;如果进入 Celery,可以启动 10 个 worker 并发处理,失败任务按策略重试,Web 进程只负责投递任务,响应保持稳定。
五、和 async/await 的区别
async/await 解决的是单个进程内等待 I/O 时如何让出执行权,BackgroundTasks 解决的是响应返回后继续做一点工作,Celery 解决的是任务持久化、分发、重试和独立执行。三者不是同一层概念。
async/await -> 当前服务进程内的异步 I/O 调度
BackgroundTasks -> 响应后在当前服务进程执行
Celery/RQ -> 跨进程/跨机器的可靠任务系统
如果你在 async 接口里直接执行 CPU 密集任务,事件循环仍然会被卡住;如果你把 CPU 密集任务放进 BackgroundTasks,响应可能先返回,但服务进程仍然会被这段 CPU 工作占用。CPU 密集或长耗时任务应该交给独立 worker。
六、工程上怎么设计可靠任务
可靠任务通常要考虑幂等、重试、超时、死信、监控和任务去重。比如“发送订单支付成功通知”,worker 可能执行到一半崩溃,外部接口可能超时,消息可能重复投递;任务函数必须能安全重试。
@celery_app.task(bind=True, autoretry_for=(TimeoutError,), retry_backoff=True)
def notify_payment_success(self, order_id: int):
order = load_order(order_id)
if order.notification_sent:
return
call_partner(order)
mark_notification_sent(order_id)
这里的关键不是 Celery 语法,而是状态设计:任务重复跑 2 次不能造成重复扣款、重复发券、重复创建外部单据。面试里提到幂等和可观测性,会比只说“用 Celery”更像真实工程经验。
七、常见误区与追问
- 误区:BackgroundTasks 是异步任务队列。 它只是响应后在当前应用进程执行任务,没有队列持久化和分布式 worker 能力。
- 误区:任务放后台就不会影响服务性能。 长耗时或 CPU 密集任务仍会占用 Web 进程资源,影响后续请求。
- 误区:用了 async def 就不需要 Celery。 async 只改善 I/O 等待调度,不能提供持久化、重试、独立 worker 和监控。
- 追问:进程重启时 BackgroundTasks 会怎样? 可能丢失,因为任务没有可靠持久化。
- 追问:发邮件用 BackgroundTasks 还是 Celery? 非关键通知可以临时用 BackgroundTasks;重要通知要用任务队列,并设计重试和幂等。
- 追问:任务队列为什么要幂等? broker 或 worker 故障可能导致重复投递或重复执行,任务必须能安全重试。
- 追问:FastAPI 里 CPU 密集任务怎么处理? 放到进程池、独立 worker 或专门计算服务,不要阻塞事件循环和 Web worker。
八、加强记忆
把三者分层记:async/await 管“等待 I/O 时怎么切换”,BackgroundTasks 管“响应后做一点轻量小尾巴”,Celery/RQ 管“可靠、可重试、可观测的独立任务”。FastAPI 面试里不要把 BackgroundTasks 吹成任务队列,它的优点是轻,边界也是轻;关键业务异步化要上 broker、worker、幂等和监控。