FastAPI 性能扩展 Redis 缓存、Celery 异步任务与容器部署
任务详情接口被频繁刷新时,数据库其实在重复回答同一个问题。把所有耗时工作都塞进 FastAPI 请求里也会出事,用户点一次导出,浏览器就一直转圈。这一篇给任务 API 加两条分流,Redis 负责短暂记忆,Celery 负责把耗时工作交给 worker。
配套代码已经放在 fastapi-task-api,文章中的完整实现以main分支为准。
Redis 不是越早接越好
缓存会带来失效问题,所以项目只缓存一个最容易定义边界的对象,任务详情。列表带分页和筛选条件,缓存键会迅速膨胀,先不碰它。
defcache_key(owner_id:uuid.UUID,task_id:uuid.UUID)->str:returnf"task:{owner_id}:{task_id}"@router.get("/{task_id}",response_model=TaskRead)asyncdefread_task(task_id:uuid.UUID,session=Depends(get_db_session),redis=Depends(get_redis)):key=cache_key(current_user.id,task_id)cached=awaitredis.get(key)ifcached:returnTaskRead.model_validate_json(cached)task=awaitowned_task(session,task_id,current_user.id)result=TaskRead.model_validate(task)awaitredis.set(key,result.model_dump_json(),ex=300)# 缓存五分钟returnresult键里必须有用户 ID。即使任务 UUID 已经很难猜,隔离条件也不能依赖运气。更新和删除任务后立刻删键,下一次读取自动回源数据库。创建接口没有对应缓存,因此不需要做任何操作。
awaitsession.commit()awaitredis.delete(cache_key(current_user.id,task_id))缓存的正确性来自失效策略,不来自缓存命中率。这就是为什么这里只放详情,不拿 Redis 去替代数据库。
BackgroundTasks 和 Celery 各干什么
FastAPI 的BackgroundTasks适合短小、允许跟 Web 进程同生共死的事情,例如记录一条审计日志。它不需要单独部署 worker。
Celery 则把任务投给 broker,由独立 worker 消费。导出 CSV、批量发邮件、调用慢速第三方接口都更适合这个路径。worker 可独立扩容,也能配置重试。任务 API 用 Redis 同时充当 broker 和结果后端。
| 场景 | 选择 |
|---|---|
| 写一条轻量审计日志 | BackgroundTasks |
| 导出任务 CSV | Celery |
| 批量处理、失败重试 | Celery |
| 需要立即返回计算结果 | 普通async接口 |
把导出变成异步工作
提交导出时,接口立即返回202和task_id。客户端再用这个 ID 查询状态。为了避免任何登录用户猜到 ID 后读取结果,项目把任务所有者短暂记录在 Redis 里。
@router.post("/exports/tasks",status_code=202)asyncdefqueue_export(redis=Depends(get_redis),current_user=Depends(get_current_user)):result=export_user_tasks.delay(str(current_user.id))awaitredis.set(f"export-owner:{result.id}",str(current_user.id),ex=3600)return{"task_id":result.id,"status":"PENDING"}worker 内部重新开数据库会话,读取该用户的任务,再生成 UTF-8 CSV 字符串。不要把 HTTP 请求里的 SQLAlchemy session 传给 Celery,它无法跨进程序列化。
@celery_app.task(bind=True,autoretry_for=(Exception,),retry_backoff=True,max_retries=3)defexport_user_tasks(self,user_id:str)->str:returnasyncio.run(build_csv(uuid.UUID(user_id)))autoretry_for只适合可重试的临时失败,例如数据库短暂不可用。CSV 格式错误或无效用户这类确定性错误,重试三次也不会变好。大文件也不该塞进结果后端,真实项目应该上传对象存储,结果只返回下载地址。
用 Docker Compose 把四个服务放到一起
本地起 API 还不难,真正容易遗漏的是 worker、PostgreSQL 和 Redis 的连接配置。docker-compose.yml定义四个服务,PostgreSQL 和 Redis 有健康检查,API 等数据库健康后先执行alembic upgrade head再启动。
Copy-Item.env.example.env docker compose up--build.env中的DATABASE_URL主机名是postgres,REDIS_URL主机名是redis,它们来自 Compose 服务名。启动成功后打开/docs,先注册、登录,复制 Bearer 令牌,再创建任务和提交导出。
这一套配置不是高可用方案。生产环境还要接日志、指标、备份、密钥管理和外部对象存储。但 API、数据库、缓存和 worker 已经各自有了明确位置,后续扩展不会全挤在 Web 进程里。
本篇收口
- Redis 只缓存任务详情,更新和删除时精确失效
- Celery 处理可延迟的 CSV 导出,并返回可查询的任务 ID
- 导出任务的所有者映射阻止其他用户读取结果
- Docker Compose 统一启动 API、PostgreSQL、Redis 与 worker
