做过几个 FastAPI 项目上线的人大概都有这样的体会------接口刚上线时响应飞快,用户一多,数据库连接池就开始报警,CPU 曲线像坐了火箭。这时候第一反应往往是加缓存,但真正动手时才发现,缓存这事儿远不是加个装饰器那么简单。到底是缓存在内存里还是丢给 Redis,缓存多久算合适,怎么保证缓存和数据库不会各说各话,这些问题处理不好,缓存反而会变成新的故障源。
FastAPI 本身没有内置的缓存机制,这既是自由也是麻烦------你得自己从社区生态里挑工具、拼方案。下面把目前主流的几条路子理一遍,再聊聊工程落地时真正该注意的细节。
缓存方案全景图
FastAPI 生态里的缓存大致可以分成三个层次,各自解决不同层面的问题。

三层缓存并不是互斥关系,实际项目里往往是叠加使用------CDN 挡住静态资源和公共接口的重复请求,HTTP 缓存头让浏览器自己判断要不要重新拉取,应用层缓存则负责扛住数据库和计算密集型逻辑的压力 。
应用层缓存:真正扛流量的那一层
内存缓存,最轻量的起点
如果项目规模不大、只跑单个进程,Python 标准库的 functools.lru_cache 或者第三方的 cachetools 就够用了。逻辑很简单,把函数的返回值按参数缓存在内存字典里,下次同样的参数直接命中,不用重新计算或查库。
python
from functools import lru_cache
@lru_cache(maxsize=128)
def get_expensive_computation(param: str):
# 耗时的计算逻辑
return result
问题在于,这种缓存活在进程里------一旦你的 FastAPI 用 Gunicorn 或 Uvicorn 起了多个 worker 进程,每个进程都有自己独立的一份缓存,数据不共享,命中率会打折扣,而且重启就全丢了。这也是为什么内存缓存基本只适合单实例、小流量的场景 。
Redis + fastapi-cache2,生产环境的标配
一旦服务要横向扩展到多个实例,缓存就必须挪到进程外部,Redis 几乎是这个位置上无可争议的选择。社区里最流行的封装库是 fastapi-cache2(也就是 GitHub 上的 long2ice/fastapi-cache),它把 Redis 缓存包装成了一个装饰器,用起来非常顺手:
python
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.decorator import cache
from redis import asyncio as aioredis
@app.on_event("startup")
async def startup():
redis = aioredis.from_url("redis://localhost", encoding="utf8")
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
@app.get("/items/{item_id}")
@cache(expire=60)
async def get_item(item_id: int):
return await fetch_from_db(item_id)
只要在路由函数上加一层 @cache,响应就会自动按参数序列化成 key 存进 Redis,过期时间到了自动失效重新计算 。这个库还支持内存、Memcached 等多种后端,切换起来只是改一行初始化代码,架构上留了不少灵活性。
不过装饰器方案有个隐性代价------控制粒度比较粗 。如果你需要在某个业务逻辑分支里精细地判断要不要缓存、缓存多久、或者手动失效某个特定 key,装饰器就显得有点笨重了。这时候更灵活的做法是直接操作 Redis 客户端,或者封装一个 CacheService 类做统一管理,Reddit 上有开发者专门整理过这三种模式的取舍------直接操作 Redis 灵活但样板代码多,装饰器省事但控制弱,服务层封装则是介于两者之间的折中方案 。
中间件方式,另一种切入角度
除了装饰器,也有人选择在中间件层拦截请求做缓存判断,好处是可以统一处理所有路由,不用挨个函数加装饰器,尤其适合那种大部分接口都需要类似缓存策略的项目。这种方式配合 Redis 存储,逻辑上是在请求进来时先查缓存命中就直接返回,不命中才放行到业务逻辑,处理完再写回缓存 。
HTTP 层缓存:让浏览器和网关替你分担
应用层缓存解决的是服务端要不要重新计算 的问题,而 HTTP 层缓存解决的是客户端要不要重新请求的问题,两者其实互补。
FastAPI 里可以直接在响应对象上设置 Cache-Control 头,告诉浏览器或者中间的 CDN、代理服务器这份数据能缓存多久:
python
from fastapi import Response
@app.get("/static-data")
async def get_static_data(response: Response):
response.headers["Cache-Control"] = "public, max-age=3600"
return {"data": "..."}
更进一步的做法是配合 ETag。ETag 本质上是给响应内容算一个指纹,客户端下次请求时带上这个指纹(If-None-Match 头),服务端一比对,内容没变就直接返回 304 Not Modified,连响应体都不用传,省了大量带宽 。
python
import hashlib
@app.get("/resource/{id}")
async def get_resource(id: int, request: Request, response: Response):
data = await fetch_data(id)
etag = hashlib.md5(str(data).encode()).hexdigest()
if request.headers.get("if-none-match") == etag:
return Response(status_code=304)
response.headers["ETag"] = etag
return data
这套机制在处理静态文件或者变化不频繁的资源时特别好使,实际项目里常见的做法是给文件路径算哈希当 ETag,配合 Last-Modified 一起用,兼容性更好 。
三种方案对比一览
| 方案 | 适用场景 | 优点 | 局限 |
|---|---|---|---|
| 内存缓存(lru_cache/cachetools) | 单实例、小流量、计算密集型函数 | 零依赖,延迟极低 | 多进程不共享,重启丢失 |
| Redis + fastapi-cache2 | 多实例生产环境、需要跨进程共享 | 分布式一致、支持过期策略、装饰器易用 | 引入外部依赖,需要维护 Redis 实例 |
| HTTP 缓存头 / ETag | 静态资源、变化不频繁的公开接口 | 减少网络请求,减轻服务端压力 | 无法应对高度个性化或实时数据 |
| CDN / 网关缓存 | 公共只读接口、静态资源分发 | 离用户最近,速度最快 | 配置复杂,动态内容不适用 |
实际工程里,这几层往往是叠加使用而不是二选一------比如商品详情页可以用 CDN 缓存图片,用 Redis 缓存商品信息接口的响应,再用 ETag 让浏览器自己判断要不要重新拉取 。
工程落地时真正该注意的坑
缓存 Key 的设计要精心
Key 的命名规则决定了缓存的粒度和命中率。常见做法是把路由路径、查询参数、甚至用户身份都编码进 key 里,避免不同用户或不同参数的请求互相污染。fastapi-cache2 默认会用函数名加参数自动生成 key,但复杂场景下最好自己接管这部分逻辑,避免出现意料之外的缓存穿透 。
缓存失效策略别偷懒
设置一个固定过期时间是最省事的做法,但很多业务场景下数据一旦更新就必须立刻让缓存失效,不能干等着过期。这就需要在数据写操作(比如更新数据库)之后,主动调用 Redis 的删除或更新接口,把对应的缓存 key 清掉。这一步经常被新手忽略,结果就是用户明明改了资料,页面显示的还是老数据。
警惕缓存雪崩和缓存穿透
如果大量缓存在同一时间点集中过期,瞬间涌入的请求会直接砸向数据库,这就是缓存雪崩 。解决办法通常是给过期时间加一点随机抖动,避免所有 key 在同一秒失效。而缓存穿透指的是请求的数据在数据库里压根不存在,导致每次都绕过缓存直接打库,这种情况可以考虑对空结果也做短时间缓存,或者用布隆过滤器提前拦截无效请求 。
异步场景下要用异步的 Redis 客户端
FastAPI 是异步框架,如果 Redis 客户端用了同步阻塞的方式调用,会拖垮整个事件循环的并发能力。fastapi-cache2 底层默认走的是 redis.asyncio,这一点在自己封装缓存逻辑时也要留意,别踩了同步阻塞的坑 。
一套务实的组合建议
对于大部分中大型 FastAPI 项目,比较靠谱的落地路径是这样搭配的------用 Redis 加 fastapi-cache2 处理数据库查询和计算密集型接口的缓存,享受装饰器的开发效率;对外暴露的公开只读接口(比如商品列表、文章详情)额外设置 Cache-Control 和 ETag,让浏览器和 CDN 分担一部分压力;数据写操作完成后主动清理相关缓存 key,避免脏数据展示给用户;过期时间统一加随机抖动,防止雪崩 。
这套组合拳打下来,既保证了数据的实时性和一致性,又能把绝大部分重复请求挡在数据库之前,是目前社区里验证过比较成熟的工程实践。
参考资料
Medium -- FastAPI Caching with Middleware(Redis)
sayanc20002.medium.com/fastapi-cac...
Reddit r/FastAPI -- FastAPI caching with more flexibility
GitHub -- long2ice/fastapi-cache
Python in Plain English -- Caching Strategies for FastAPI: Redis, In-Memory, and HTTP Cache Headers
python.plainenglish.io/caching-str...
Medium -- FastAPI Cache Design: CDN, HTTP, and In-App Layers
MojoAuth -- Implement Caching with FastAPI
Stack Overflow -- How using browser cache when fetching files from FastAPI?