三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

FastAPI 性能扩展 Redis 缓存、Celery 异步任务与容器部署

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
导出任务 CSVCelery
批量处理、失败重试Celery
需要立即返回计算结果普通async接口

把导出变成异步工作

提交导出时,接口立即返回202task_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 格式错误或无效用户这类确定性错误,重试三次也不会变好。大文件也不该塞进结果后端,真实项目应该上传对象存储,结果只返回下载地址。

Celery WorkerRedisFastAPIClientCelery WorkerRedisFastAPIClientPOST 导出投递任务和保存所有者202 task_idworker 消费任务写入结果状态GET 导出状态PENDING 或 SUCCESS

用 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主机名是postgresREDIS_URL主机名是redis,它们来自 Compose 服务名。启动成功后打开/docs,先注册、登录,复制 Bearer 令牌,再创建任务和提交导出。

这一套配置不是高可用方案。生产环境还要接日志、指标、备份、密钥管理和外部对象存储。但 API、数据库、缓存和 worker 已经各自有了明确位置,后续扩展不会全挤在 Web 进程里。

本篇收口

  • Redis 只缓存任务详情,更新和删除时精确失效
  • Celery 处理可延迟的 CSV 导出,并返回可查询的任务 ID
  • 导出任务的所有者映射阻止其他用户读取结果
  • Docker Compose 统一启动 API、PostgreSQL、Redis 与 worker
← 返回列表