FastAPI 集成 Redis 缓存实战笔记
在 FastAPI 这样的异步 Web 框架中,使用异步的 Redis 客户端 (redis.asyncio) 能够极大地提升接口响应速度并减轻数据库查询压力。最典型的使用场景是 KV 缓存(Key-Value Cache)。
下面以项目中的"新闻分类"模块为例,拆解 Redis 缓存从【配置】到【封装】再到【业务调用】的完整流程。
1. 理论基础:设计缓存策略
1.1 旁路缓存策略(Cache-Aside)
这是项目中最常见的一种缓存策略,其核心概念是应用程序主动管理缓存:
- 读取数据:先检查缓存,如果缓存中没有数据(未命中),则从数据库加载数据,并将数据存入缓存后返回。
- 更新数据:当数据更新或删除时,应用程序也负责主动更新或删除缓存中的数据。
核心工作流程图解:
1.2 缓存时间设计(TTL 策略)
核心原则 :不同类型的数据,缓存时间不同,否则如果同一时间大面积缓存过期,会出现缓存雪崩。
数据越稳定,缓存越久;数据变化越快,缓存越短。
| 类型 | 时间 |
|---|---|
| 分类、配置 | 7200 (2小时) |
| 列表数据 | 600 (10分钟) |
| 详情数据 | 1800 (30分钟) |
| 验证码 | 120 (2分钟) |
1.3 缓存 Key 的设计规范
缓存 Key 必须具有唯一性 且能表达明确的业务参数含义,各个维度之间通常用冒号 : 分隔。
- 示例 (新闻列表) :
news:list:分类 ID:页码:每页数量 - 示例 Value:对应的新闻列表 JSON 字符串。
2. 第一步:Redis 连接池配置
在 FastAPI 中,我们强烈建议使用连接池(ConnectionPool)并开启异步支持。这样在处理高并发时,不需要频繁建立和销毁连接。
📄 文件 :config/cache_conf.py
python
# 导入Redis异步客户端类
import redis.asyncio as redis
from redis.asyncio import ConnectionPool
# 1. 定义Redis连接配置
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PASSWORD = None
# 关键配置:自动解码响应为字符串,否则取出来的是 bytes 类型
REDIS_DECODE_RESPONSES = True
# 2. 定义默认缓存过期时间(秒)
NEWS_CACHE_TTL = 600 # 新闻缓存10分钟
# 3. 创建Redis异步连接池客户端
redis_client = redis.Redis(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB,
password=REDIS_PASSWORD,
decode_responses=REDIS_DECODE_RESPONSES,
max_connections=20, # 最大连接数
retry_on_timeout=True, # 超时重试
socket_keepalive=True, # 保持心跳
)
3. 第二步:缓存逻辑封装 (存、取、清)
针对特定的业务模块(比如新闻),专门抽取出一个缓存操作文件。这里包含了三种核心动作:取缓存 、存缓存 、清除缓存。
📄 文件 :cache/news_cache.py
python
import json
from typing import Optional, Any
from fastapi.encoders import jsonable_encoder
from TouTiaoApp.config.cache_conf import redis_client, NEWS_CACHE_TTL
# ==========================================
# 1. 取缓存 (Get)
# ==========================================
async def get_categories_from_cache() -> Optional[Any]:
"""从缓存获取新闻分类列表"""
try:
# 获取 Key 为 "news:categories" 的数据
data = await redis_client.get("news:categories")
if data is None:
return None
# 因为存的时候是字符串,取出时需进行 JSON 反序列化
return json.loads(data)
except Exception as e:
print(f"获取分类缓存失败: {e}")
return None
# ==========================================
# 2. 存缓存 (Set & Expire)
# ==========================================
async def set_categories_to_cache(categories: Any) -> bool:
"""将新闻分类列表存入缓存"""
try:
# FastAPI 自带工具:将 ORM 对象安全地转为可序列化的字典
data = jsonable_encoder(categories)
# 使用 setex 保证"存值"和"设置过期时间"是原子操作
await redis_client.setex(
"news:categories",
NEWS_CACHE_TTL, # 10分钟自动过期
json.dumps(data, ensure_ascii=False) # JSON 序列化存入
)
return True
except Exception as e:
print(f"设置分类缓存失败: {e}")
return False
# ==========================================
# 3. 清理/失效缓存 (Delete)
# ==========================================
async def clear_news_cache() -> bool:
"""清除所有新闻相关的缓存(通常在后台修改了数据后调用)"""
try:
# 模糊查找所有 news: 开头的键
keys = await redis_client.keys("news:*")
if keys:
# *keys 是参数解包,实现批量删除
await redis_client.delete(*keys)
return True
except Exception as e:
print(f"清除新闻缓存失败: {e}")
return False
4. 第三步:在路由接口中应用
在真实的 API 接口中,将前面的逻辑串联起来,完成完整的缓存闭环。
📄 文件 :routers/news.py
python
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from TouTiaoApp.config.db_conf import post_dbs
from TouTiaoApp.curd import news
from TouTiaoApp.cache.news_cache import get_categories_from_cache, set_categories_to_cache
from TouTiaoApp.utils.response import success_response
router = APIRouter(prefix="/api/news", tags=["news"])
@router.get("/categories")
async def get_categories(
db: AsyncSession = Depends(post_dbs),
skip: int = 0,
limit: int = 10
):
"""获取新闻分类列表接口"""
# 🌟 1. 尝试从缓存获取分类数据
cached_categories = await get_categories_from_cache()
if cached_categories is not None:
# 命中缓存,直接返回,速度极快!
return success_response(data=cached_categories)
# 🌟 2. 缓存未命中,从 MySQL 数据库查询
categories = await news.get_categories(db, skip, limit)
# 🌟 3. 将查询结果回写存入 Redis 缓存,以便下次请求直接命中
await set_categories_to_cache(categories)
# 🌟 4. 返回真实数据
return success_response(message="获取新闻分类成功", data=categories)
5. 核心知识点梳理 (面试常考)
-
异步支持 (
redis.asyncio) FastAPI 是一个异步框架。如果在 FastAPI 里使用了普通的同步 Redis 库,Redis 的 IO 阻塞会导致整个 FastAPI 的并发能力下降。因此必须使用redis.asyncio并且所有的 Redis 操作前面都要加上await。 -
序列化与反序列化 Redis 的 String 类型只接受字符串或字节。所以存入复杂的对象(如列表、字典、ORM 对象)时,必须先用
json.dumps()将其转化为 JSON 字符串。读取出来后,再用json.loads()还原。项目中借助了 FastAPI 提供的jsonable_encoder来将对象转为安全的字典格式。 -
设置过期时间的重要原则 只要向 Redis 里写缓存,绝大多数场景下必须设置过期时间 (TTL) ,以此来保证最终一致性(防止脏数据永远存在)以及内存回收。代码中使用了
setex(key, time, value)命令,它能够原子性地把赋值和设过期时间一步搞定,避免了分两步执行导致异常中断的问题。 -
缓存的主动清理 当管理后台修改了某个分类,如果不清理缓存,用户看到的还是旧数据。此时需要调用
clear_news_cache()。代码中利用keys("news:*")找出这一类的所有 Key,然后用delete批量抹除缓存,下一次用户请求时就会强制去 MySQL 查出最新数据并刷新缓存了。 -
Redis 连接池与最大连接数 (
max_connections) 在配置客户端时传入的redis.Redis(..., max_connections=20)实际上会在底层隐式创建连接池。这表示当前 FastAPI 应用程序最多只会同时建立 20 条与 Redis 服务器的 TCP 连接。- 连接复用:当请求需要查询缓存时,会从池中"借用"一条连接,因为 Redis 处理极快(微秒级),使用完毕后连接会立刻"归还",供下个请求复用。
- 保护服务器:在高并发场景下,如果几千个请求同时到达,前 20 个瞬间拿到连接,后面的请求会短暂排队。限制最大连接数能彻底防止程序因为无节制地创建新连接,导致 Redis 或 Web 服务器内存及系统资源耗尽而宕机。
6. 进阶实战:高并发与核心业务场景解析
6.1 订单超时取消(例如 30 分钟过期)
- 业务背景:在电商或抢票系统中,用户提交订单后系统会暂时锁定库存。如果用户长时间未付款,系统必须自动取消订单并释放库存,以防商品"卖不出去"。
- Redis 实现思路 :利用 Redis 的键过期机制 (expire) 或者之前学过的 延迟队列 (zset)。下单时将订单号存入 Redis 并设置 30 分钟 TTL(生存时间)。当 30 分钟到期后,系统通过过期事件或后台轮询发现未支付的订单,即可自动执行取消和回滚库存的逻辑。
6.2 数据一致性与高并发下的数据库保护(秒杀场景)
- "并发大的话,操作数据库容易出问题" 在"双11"秒杀这种极高并发(如瞬间上万次请求)的场景下,如果直接操作 MySQL 扣减库存(如执行
UPDATE stock = stock - 1),所有请求都会去抢占数据库中同一行记录的"行锁",导致数据库连接被打满,最终使数据库卡死甚至宕机。 - Redis 的终极解决方案 :
- 内存抗压:在活动开始前,将 MySQL 中的"库存总量"提前预热加载到 Redis 中。
- 原子扣减(防超卖) :海量并发涌入时,所有的扣库存操作只在 Redis 中进行,不碰 MySQL 。因为 Redis 单线程处理
DECR(自减)命令是绝对原子的,能够保证即使一万人同时抢,库存到 0 后也会完美拦截后续请求,绝不会产生"超卖"导致数据不一致。 - 异步排队写回:在 Redis 扣减成功的极少数请求,会被放入消息队列(MQ)中,后台服务再慢条斯理地排队操作 MySQL 真正生成订单。这样就把洪峰流量挡在了数据库外面。