2. Redis 缓存实战

FastAPI 集成 Redis 缓存实战笔记

在 FastAPI 这样的异步 Web 框架中,使用异步的 Redis 客户端 (redis.asyncio) 能够极大地提升接口响应速度并减轻数据库查询压力。最典型的使用场景是 KV 缓存(Key-Value Cache)

下面以项目中的"新闻分类"模块为例,拆解 Redis 缓存从【配置】到【封装】再到【业务调用】的完整流程。


1. 理论基础:设计缓存策略

1.1 旁路缓存策略(Cache-Aside)

这是项目中最常见的一种缓存策略,其核心概念是应用程序主动管理缓存

  • 读取数据:先检查缓存,如果缓存中没有数据(未命中),则从数据库加载数据,并将数据存入缓存后返回。
  • 更新数据:当数据更新或删除时,应用程序也负责主动更新或删除缓存中的数据。

核心工作流程图解

graph LR A[前端请求数据] --> B{查缓存} B -- 无缓存 --> C[查数据库] C --> D[写入缓存] D --> E[返回数据给前端] B -- 有缓存 -------------------------> E

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. 核心知识点梳理 (面试常考)

  1. 异步支持 (redis.asyncio) FastAPI 是一个异步框架。如果在 FastAPI 里使用了普通的同步 Redis 库,Redis 的 IO 阻塞会导致整个 FastAPI 的并发能力下降。因此必须使用 redis.asyncio 并且所有的 Redis 操作前面都要加上 await

  2. 序列化与反序列化 Redis 的 String 类型只接受字符串或字节。所以存入复杂的对象(如列表、字典、ORM 对象)时,必须先用 json.dumps() 将其转化为 JSON 字符串。读取出来后,再用 json.loads() 还原。项目中借助了 FastAPI 提供的 jsonable_encoder 来将对象转为安全的字典格式。

  3. 设置过期时间的重要原则 只要向 Redis 里写缓存,绝大多数场景下必须设置过期时间 (TTL) ,以此来保证最终一致性(防止脏数据永远存在)以及内存回收。代码中使用了 setex(key, time, value) 命令,它能够原子性地把赋值和设过期时间一步搞定,避免了分两步执行导致异常中断的问题。

  4. 缓存的主动清理 当管理后台修改了某个分类,如果不清理缓存,用户看到的还是旧数据。此时需要调用 clear_news_cache()。代码中利用 keys("news:*") 找出这一类的所有 Key,然后用 delete 批量抹除缓存,下一次用户请求时就会强制去 MySQL 查出最新数据并刷新缓存了。

  5. 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 的终极解决方案
    1. 内存抗压:在活动开始前,将 MySQL 中的"库存总量"提前预热加载到 Redis 中。
    2. 原子扣减(防超卖) :海量并发涌入时,所有的扣库存操作只在 Redis 中进行,不碰 MySQL 。因为 Redis 单线程处理 DECR(自减)命令是绝对原子的,能够保证即使一万人同时抢,库存到 0 后也会完美拦截后续请求,绝不会产生"超卖"导致数据不一致。
    3. 异步排队写回:在 Redis 扣减成功的极少数请求,会被放入消息队列(MQ)中,后台服务再慢条斯理地排队操作 MySQL 真正生成订单。这样就把洪峰流量挡在了数据库外面。
相关推荐
晚安code1 小时前
Java 并发编程必会的 JUC 辅助类:从 Callable 到阻塞队列
后端
叱咤月海鱼鱼猫1 小时前
后端生成图片传递到前端
后端
长栎1 小时前
你写的 AI 品控规则三个月就过期——不是规则错了,是你没给它做"回归测试"
后端
沙湖遇雨1 小时前
7.EventLoop 生命周期
后端
雨落倾城夏未凉1 小时前
halcon核心-模板匹配定位(二)
后端
ma_king1 小时前
Spring Boot 接入飞书自定义机器人
java·后端
2401_894915531 小时前
部署 GEO 优化源码常见报错排查:端口、伪静态、缓存问题解决
java·运维·服务器·后端·缓存·开源
卷无止境1 小时前
聊聊Web开发里的流式数据 从原理到FastAPI实战
后端·python·fastapi
Scene2161 小时前
AgentScope 2.0:4. Message & Event —— 消息模型与事件流深度解析
后端