65. FastAPI 结合 Redis 使用
Redis 常被当作"放在内存里的公共小仓库",用来做缓存、计数器、会话这类需要快读快写的数据。本篇用一个完整项目,从配置到封装再到三个真实场景,一步步把 Redis 接进 FastAPI。
一、Redis 基础配置
安装准备
uv add redis pydantic-settingsredis是官方客户端,异步接口在redis.asyncio(旧项目里的aioredis已合并进来,不要再单独装)。- 确保本地 Redis 已启动,可用
redis-cli ping验证(返回PONG即正常)。没装 Redis 可用 Docker 快速起一个。
连接参数
| 参数 | 含义 | 默认值 |
|---|---|---|
host | 主机地址 | 127.0.0.1 |
port | 端口 | 6379 |
password | 密码,无则留空 | 空 |
db | 数据库编号 | 0 |
db 是 Redis 内部的逻辑库编号(0~15),不是 MySQL 那种独立数据库,仅用于把不同业务的键隔离开。
集中管理配置
用 pydantic-settings 把连接参数集中到一个 Settings,从 .env 读取。
core/config.py:
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
redis_host: str = "127.0.0.1"
redis_port: int = 6379
redis_password: str = ""
redis_db: int = 0
@lru_cache
def get_settings() -> Settings:
return Settings().env.example(复制为 .env 后按需修改):
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0get_settings() 用 @lru_cache 缓存实例,整个进程只解析一次 .env。密码这类敏感信息走环境变量,不硬编码进代码。
二、封装 RedisClient 模块
把连接和常用操作封装成一个类,路由和依赖注入直接复用,业务代码不再关心底层的 redis.asyncio 细节。
core/redis_client.py:
import json
import redis.asyncio as aioredis
from core.config import get_settings
class RedisClient:
def __init__(self) -> None:
self._settings = get_settings()
self._client: aioredis.Redis | None = None
async def connect(self) -> None:
self._client = aioredis.Redis(
host=self._settings.redis_host,
port=self._settings.redis_port,
password=self._settings.redis_password or None,
db=self._settings.redis_db,
decode_responses=True,
)
await self._client.ping()
async def close(self) -> None:
if self._client is not None:
await self._client.aclose()
self._client = None
@property
def client(self) -> aioredis.Redis:
if self._client is None:
raise RuntimeError("Redis 尚未连接,请先调用 connect()")
return self._client
# ---------- 基础操作 ----------
async def get(self, key: str) -> str | None:
return await self.client.get(key)
async def set(self, key: str, value: str, expire: int | None = None) -> bool:
return await self.client.set(key, value, ex=expire)
async def delete(self, *keys: str) -> int:
return await self.client.delete(*keys)
async def exists(self, key: str) -> bool:
return bool(await self.client.exists(key))
async def expire(self, key: str, seconds: int) -> bool:
return await self.client.expire(key, seconds)
async def incr(self, key: str) -> int:
return await self.client.incr(key)
# ---------- JSON 便捷方法 ----------
async def set_json(self, key: str, value, expire: int | None = None) -> bool:
return await self.client.set(
key, json.dumps(value, ensure_ascii=False), ex=expire
)
async def get_json(self, key: str):
data = await self.client.get(key)
return json.loads(data) if data is not None else None
redis_client = RedisClient()
async def get_redis() -> RedisClient:
return redis_client要点:
decode_responses=True让get直接返回字符串而不是bytes。get_json/set_json内部用json.dumps/json.loads处理字典,因为标准 Redis 只存字符串。- 末尾的
redis_client是单例,get_redis是给Depends用的依赖函数。
main.py 在 lifespan 里连接、关闭:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from core.redis_client import redis_client
from routers import redis_demo
@asynccontextmanager
async def lifespan(app: FastAPI):
await redis_client.connect()
yield
await redis_client.close()
app = FastAPI(lifespan=lifespan)
app.include_router(redis_demo.router)三、实际使用场景
三个场景都写在 routers/redis_demo.py,通过 Depends(get_redis) 拿到同一个 RedisClient 实例。
import asyncio
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException
from core.redis_client import RedisClient, get_redis
router = APIRouter(tags=["Redis 示例"])
# ---------- 场景1:缓存 ----------
async def load_from_db(user_id: int):
await asyncio.sleep(1) # 模拟一次慢查询
return {"id": user_id, "name": f"用户{user_id}"}
@router.get("/cache/user/{user_id}")
async def get_user(user_id: int, redis: RedisClient = Depends(get_redis)):
key = f"cache:user:{user_id}"
cached = await redis.get_json(key)
if cached is not None:
return {"source": "cache", **cached}
data = await load_from_db(user_id)
await redis.set_json(key, data, expire=60)
return {"source": "db", **data}
# ---------- 场景2:计数器 / 限流 ----------
@router.get("/counter/visit")
async def visit(redis: RedisClient = Depends(get_redis)):
total = await redis.incr("counter:visit")
return {"visit_count": total}
@router.get("/counter/limited")
async def limited(redis: RedisClient = Depends(get_redis)):
key = "counter:limited"
count = await redis.incr(key)
if count == 1:
await redis.expire(key, 60) # 首次访问后 60 秒过期
if count > 5:
raise HTTPException(status_code=429, detail="请求过于频繁")
return {"allowed": True, "count": count}
# ---------- 场景3:会话存储 ----------
@router.post("/session/login")
async def login(name: str, redis: RedisClient = Depends(get_redis)):
token = uuid4().hex
await redis.set_json(f"session:{token}", {"name": name}, expire=3600)
return {"token": token}
@router.get("/session/me")
async def me(token: str, redis: RedisClient = Depends(get_redis)):
data = await redis.get_json(f"session:{token}")
if data is None:
raise HTTPException(status_code=401, detail="会话不存在或已过期")
return data场景1:缓存
第一次请求会走"慢查询"(约 1 秒),并把结果写进 Redis;之后 60 秒内直接命中缓存。
curl.exe http://127.0.0.1:8000/cache/user/1第一次返回(约 1 秒,source 是 db):
{ "source": "db", "id": 1, "name": "用户1" }再请求一次(几乎瞬间,source 是 cache):
{ "source": "cache", "id": 1, "name": "用户1" }场景2:计数器
incr 是原子的自增,天然适合计数和简单限流。
curl.exe http://127.0.0.1:8000/counter/visit连续请求依次返回 {"visit_count": 1}、{"visit_count": 2}、{"visit_count": 3}……
限流接口连续请求 6 次:
curl.exe http://127.0.0.1:8000/counter/limited前 5 次返回 {"allowed": true, "count": N},第 6 次返回 429:
{ "detail": "请求过于频繁" }场景3:会话存储
登录生成一个带 1 小时过期时间的 token:
curl.exe "http://127.0.0.1:8000/session/login?name=张三"返回:
{ "token": "6a2c...e9f1" }带上 token 查询会话:
curl.exe "http://127.0.0.1:8000/session/me?token=6a2c...e9f1"返回 {"name": "张三"}。token 不存在或过期后,返回 401。
四、注意事项
连接管理
- 用 lifespan 创建、关闭连接,不要在每个请求里
connect()/ 新建客户端。redis.asyncio.Redis自带连接池,复用连接才有意义。 - 多 worker(
uvicorn --workers N)时,每个进程各自持有连接;不要把连接对象跨进程共享。 close()只在应用退出时调用一次,不要放进请求路径。
异常处理
缓存属于"锦上添花",Redis 不可用时不应拖垮主流程。读缓存建议降级处理:
async def safe_get_json(redis, key: str):
try:
return await redis.get_json(key)
except Exception:
return None # Redis 异常时降级,保证接口可用写缓存失败也应吞掉异常或只记日志,让业务继续。是否降级取决于场景:会话、限流这类强依赖 Redis 的功能,失败时则要明确报错。
键命名规范
- 用冒号分层:
业务:模块:标识,例如cache:user:1、counter:visit、session:<token>。 - 统一前缀便于按前缀清理、监控,也避免不同业务键名冲突。
- 生产环境不要用
KEYS cache:*全量扫描(会阻塞 Redis),用SCAN游标遍历。 - 合理设置过期时间,避免缓存键无限增长占满内存。
其他
- 更新数据时优先"删除缓存"而不是"更新缓存",让下一次读取自然重建,减少缓存与数据库不一致。
- 密码走环境变量,不写进代码或提交到仓库。
练习:给缓存接口加一个 DELETE /cache/user/{user_id} 主动删除缓存;把限流阈值和窗口改成可配置;用 redis-cli 查看 cache:user:1 的剩余过期时间(TTL cache:user:1)。