新闻头条后端:新闻缓存模块

新闻头条后端:新闻缓存模块

缓存模块

数据缓存

缓存是一种存储机制,用于临时存储数据或计算结果,当再次需要这些数据时,可以快速从缓存中检索,而不是重新进行耗时或昂贵的获取和计算过程。在网站开发中,缓存(Cache)是一个非常重要的概念,其核心作用是提高性能、降低延迟和减轻服务器负载。

主要优势:

  1. 提升性能和用户体验
  2. 减轻服务器/数据库负载
  3. 降低网络延迟
  4. 节省资源和成本

Redis配置流程

1. 安装

1.1 安装服务端,这里省略

启动服务端

powershell 复制代码
C:\Users\user>redis-server
[23040] 11 Jul 19:22:29.404 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
[23040] 11 Jul 19:22:29.406 # Redis version=5.0.14, bits=64, commit=a7c01ef4, modified=0, pid=23040, just started
[23040] 11 Jul 19:22:29.406 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 5.0.14 (a7c01ef4/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 23040
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           http://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

[23040] 11 Jul 19:22:29.410 # Server initialized
[23040] 11 Jul 19:22:29.423 * DB loaded from disk: 0.013 seconds
[23040] 11 Jul 19:22:29.423 * Ready to accept connections

1.2 安装redis包

bash 复制代码
pip install redis==7.1.0

2. 配置文件

config.cache_conf

python 复制代码
import json
from typing import Any

import redis.asyncio as redis

REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 1


# 创建 Redis 的连接对象
redis_client = redis.Redis(
    host=REDIS_HOST,  # Redis 服务器的主机地址
    port=REDIS_PORT,  # Redis 端口号
    db=REDIS_DB,  # Redis 数据库编号,0~15
    decode_responses=True,  # 是否将字节数据解码为字符串
    protocol=2  # 👈 关键:强制使用 RESP2 协议,兼容 Redis 5.x
)

注意:这里的redis版本,可能导致缓存无法生效,请确保版本能用。这里用protocol=2 来强制使用 RESP2 协议,兼容 Redis 5.x。若pip install redis会安装最新的版本,如:8.1.0的,就会造成新旧版本的不兼容问题。这里虽然服务端是5.0.14的,但是可以用,通过protocol=2 进行配置。

可以先测试一下连接是否成功,详见文章最后一节

3. 缓存操作

设置缓存,读取缓存,删除缓存

缓存操作就是围绕 Redis 做"存、取、删、判断、过期"等操作,让数据访问更快、数据库压力更小。

Redis 存储数据:key - value

config.cache_conf

python 复制代码
# 设置 和 读取(字符串 和 列表或字典)"[{}]"
# 读取:字符串
async def get_cache(key: str):
    # return await redis_client.get(key)
    try:
        return await redis_client.get(key)
    except Exception as e:
        print(f"获取缓存失败:{e}")
        return None


# 读取:列表或字典
async def get_json_cache(key: str):
    try:
        data = await redis_client.get(key)
        if data:
            return json.loads(data)  # 反序列化
        return None
    except Exception as e:
        print(f"获取 JSON 缓存失败:{e}")
        return None


# 设置缓存 setex(key, expire, value) 3600秒=1小时
async def set_cache(key: str, value: Any, expire: int = 3600):
    try:
        if isinstance(value, (dict, list)):
            # 转字符串再存 序列化
            value = json.dumps(value, ensure_ascii=False)  # 中文正常保存
        await redis_client.setex(key, expire, value)
        return True
    except Exception as e:
        print(f"设置缓存失败:{e}")
        return False

4. 设计缓存策略

旁路缓存策略(Cache-Aside) 是一种常见的缓存策略,其核心概念是应用程序主动管理缓存,在读取数据时先检查缓存,如果缓存中没有数据,则从数据库或其他数据源加载数据,并将数据存入缓存;当数据更新或删除时,应用程序也负责更新或删除缓存中的数据。

系统中加入缓存

新闻分类

不同类型的数据,缓存时间不同,否则会出现缓存雪崩

数据越稳定,缓存越久;数据变化越快,缓存越短

此外要注意,不同类型的数据要设置不同的时间,以防止同时到期后造成雪崩

cache.news_cache

python 复制代码
# 新闻相关的缓存方法:新闻分类的读取和写入
# key - value
from typing import List, Dict, Any, Optional
from config.cache_conf import get_json_cache, set_cache

CATEGORIES_KEY = "news:categories"

# 获取新闻分类缓存
async def get_cached_categories():
    return await get_json_cache(CATEGORIES_KEY)

# 写入新闻分类缓存: 缓存的数据, 过期时间
# 分类、配置 7200;列表: 600; 详情: 1800;验证码:120 -- 数据越稳定,缓存越持久
# 避免所有key同时过期 引起缓存雪崩
async def set_cache_categories(data: List[Dict[str, Any]], expire: int = 7200):
    return await set_cache(CATEGORIES_KEY, data, expire)

curd.news_cache

python 复制代码
async def get_categories(db: AsyncSession, skip: int = 0, limit: int = 100):
    # 先尝试从缓存中获取数据
    cached_categories = await get_cached_categories()
    if cached_categories:
        return cached_categories

    stmt = select(Category).offset(skip).limit(limit)
    result = await db.execute(stmt)
    categories = result.scalars().all()  # ORM

    # 写入缓存
    if categories:
        # 将ORM对象转换为字典
        categories = jsonable_encoder(categories)
        await set_cache_categories(categories)

    # 返回数据
    return categories

新闻列表

设计缓存策略 - 新闻列表

  • 缓存 Key(唯一):news:list:分类 ID:页码:每页数量
  • 缓存 Value:新闻列表

cache.news_cache

python 复制代码
NEWS_LIST_PREFIX = "news_list:"

# 写入缓存-新闻列表 key = news_list:分类id:页码:每页数量  + 列表数据 + 过期时间
async def set_cache_news_list(category_id: Optional[int], page: int, size: int, news_list: List[Dict[str, Any]], expire: int = 1800):
    # 调用 封装的 Redis 的设置方法,存新闻列表到缓存
    category_part = category_id if category_id is not None else "all"
    key = f"{NEWS_LIST_PREFIX}{category_part}:{page}:{size}"
    return await set_cache(key, news_list, expire)


# 读取缓存-新闻列表
async def get_cache_news_list(category_id: Optional[int], page: int, size: int):
    category_part = category_id if category_id is not None else "all"
    key = f"{NEWS_LIST_PREFIX}{category_part}:{page}:{size}"
    return await get_json_cache(key)

curd.news_cache

python 复制代码
async def get_news_list(db: AsyncSession, category_id: int, skip: int = 0, limit: int = 10):
    # 先尝试从缓存获取新闻列表
    # 跳过的数量skip = (页码 -1) * 每页数量 → 页码 = 跳过的数量 // 每页数量 + 1
    # await get_cache_news_list(分类id, 页码, 每页数量)
    page = skip // limit + 1
    # 缓存数据 json
    cached_list = await get_cache_news_list(category_id, page, limit)
    if cached_list:
        print("从缓存中获取新闻列表")
        print(cached_list)
        # return cached_list  # 要的是 ORM
        # 缓存数据转换成 ORM   [{"id": 1, "title": "新闻1",}, {"id": 2, "title": "新闻2",}]  ->  [News(id=1, title="新闻1",), News(id=2, title="新闻2",)]
        # item: {"id": 1, "title": "新闻1",},   **item: id: 1, title: 新闻1,}
        # 将读取到的**item的json数据转换成ORM对象
        return [News(**item) for item in cached_list]

    # 查询的是指定分类下的所有新闻
    stmt = select(News).where(News.category_id ==
                              category_id).offset(skip).limit(limit)
    result = await db.execute(stmt)
    news_list = result.scalars().all()

    # 写入缓存
    if news_list:
        # 先把 ORM 数据 转换 字典才能写入缓存
        # ORM 转成 Pydantic,即NewsItemBase.model_validate(item),再通过model_dump()转为字典
        # by_alias=False 不适用别名,保存 Python 风格,因为 Redis 数据是给后端用的
        news_data = [NewsItemBase.model_validate(item).model_dump(
            mode="json", by_alias=False) for item in news_list]
        await set_cache_news_list(category_id, page, limit, news_data)

    return news_list

新闻详情

cache.news_cache:

python 复制代码
NEWS_DETAIL_PREFIX = "news:detail:"

async def get_cached_news_detail(news_id: int) -> Optional[Dict[str, Any]]:
    """
    获取缓存的新闻详情

    Args:
        news_id: 新闻ID

    Returns:
        Optional[Dict[str, Any]]: 新闻数据,不存在则返回None
    """
    key = f"{NEWS_DETAIL_PREFIX}{news_id}"
    return await get_json_cache(key)


async def cache_news_detail(news_id: int, news_data: Dict[str, Any], expire: int = 300) -> bool:
    """
    缓存新闻详情

    Args:
        news_id: 新闻ID
        news_data: 新闻数据字典
        expire: 过期时间(秒),默认5分钟

    Returns:
        bool: 缓存成功返回True
    """
    key = f"{NEWS_DETAIL_PREFIX}{news_id}"
    return await set_cache(key, news_data, expire)

新闻详情的urd操作:

python 复制代码
async def get_news_count(db: AsyncSession, category_id: int):
    # 查询的是指定分类下的新闻数量
    stmt = select(func.count(News.id)).where(News.category_id == category_id)
    result = await db.execute(stmt)
    return result.scalar_one()  # 只能有一个结果,否则报错


async def get_news_detail(db: AsyncSession, news_id: int):
    # 先尝试从缓存获取
    cached_news = await get_cached_news_detail(news_id)
    if cached_news:
        # 缓存数据可能包含 related_news,需要过滤掉(News 模型没有这个字段)
        # filtered_data = {k: v for k, v in cached_news.items() if k != 'related_news'}
        # return News(**filtered_data)
        return News(**cached_news)

    stmt = select(News).where(News.id == news_id)
    result = await db.execute(stmt)
    news = result.scalar_one_or_none()

    # 如果查询到数据,存入缓存(不使用别名,保持数据库字段名)
    if news:
        # 构造新闻详情数据用于缓存(包含 content 字段)
        # news_dict = {k: v for k, v in news.__dict__.items() if not k.startswith('_')}
        news_dict = NewsDetailResponse.model_validate(news).model_dump(
            by_alias=False, mode="json", exclude={'related_news'}
        )
        await cache_news_detail(news_id, news_dict)

    return news

相关新闻

cache.news_cache:

python 复制代码
RELATED_NEWS_PREFIX = "news:related:"

async def cache_related_news(news_id: int, category_id: int, related_list: List[Dict[str, Any]], expire: int = 1800) -> bool:
    """
    缓存相关新闻列表

    Args:
        news_id: 当前新闻ID
        category_id: 新闻分类ID
        related_list: 相关新闻列表数据
        expire: 过期时间(秒)

    Returns:
        bool: 缓存成功返回True
    """
    key = f"{RELATED_NEWS_PREFIX}{news_id}:{category_id}"
    return await set_cache(key, related_list, expire)


async def get_cached_related_news(news_id: int, category_id: int) -> Optional[List[Dict[str, Any]]]:
    """
    获取缓存的相关新闻列表

    Args:
        news_id: 当前新闻ID
        category_id: 新闻分类ID

    Returns:
        Optional[List[Dict[str, Any]]]: 相关新闻列表数据,不存在则返回None
    """
    key = f"{RELATED_NEWS_PREFIX}{news_id}:{category_id}"
    return await get_json_cache(key)

相关新的urd操作

python 复制代码
async def increase_news_views(db: AsyncSession, news_id: int):
    stmt = update(News).where(News.id == news_id).values(views=News.views + 1)
    result = await db.execute(stmt)
    await db.commit()

    # 更新 → 检查数据库是否真的命中了数据 → 命中了返回True
    return result.rowcount > 0


async def get_related_news(db: AsyncSession, news_id: int, category_id: int, limit: int = 5):
    cached_related = await get_cached_related_news(news_id, category_id)
    if cached_related:
        # 缓存数据是字典列表,直接返回
        return cached_related
    # order_by 排序 → 浏览量和发布时间
    stmt = select(News).where(
        News.category_id == category_id,
        News.id != news_id
    ).order_by(
        News.views.desc(),  # 默认是升序,desc 表示降序
        News.publish_time.desc()
    ).limit(limit)
    result = await db.execute(stmt)
    # return result.scalars().all()
    related_news = result.scalars().all()

    # 转换为字典格式用于缓存和返回(不使用别名,保持数据库字段名)
    if related_news:
        related_data = [
            RelatedNewsResponse.model_validate(
                news).model_dump(by_alias=False, mode="json")
            for news in related_news
        ]
        await cache_related_news(news_id, category_id, related_data)
        return related_data

    # 没有相关新闻,返回空列表
    return []
    # 列表推导式 推导出新闻的核心数据,然后再 return
    # return [{
    #     "id": news_detail.id,
    #     "title": news_detail.title,
    #     "content": news_detail.content,
    #     "image": news_detail.image,
    #     "author": news_detail.author,
    #     "publishTime": news_detail.publish_time,
    #     "categoryId": news_detail.category_id,
    #     "views": news_detail.views
    # } for news_detail in related_news]

修改路由

使用别名的方式,使得带缓存的curd生效

routers.news:

python 复制代码
# 不使用缓存
# from crud import news
# 使用缓存
from crud import news_cache as news

# 创建 APIRouter 实例
# prefix 路由前缀(API 接口规范文档)
# tags 分组 标签
router = APIRouter(prefix="/api/news", tags=["news"])

测试效果

刷新浏览器http://127.0.0.1:5173/home会请求后端地址:http://127.0.0.1:8000/api/news/categories,此时,会通过设计的旁路缓存策略使得redis缓存中新增缓存数据。

或者用redisclient连接查看

踩坑

Redis版本问题

服务器为5.0.14,刚开始安装的redis为8.0以上的版本,造成的报错,让后退回7.x版本后,不报错,但是缓存一直不生效。

原因:服务器和依赖的版本不兼容问题,redis连接中设置参数protocol=2 即可。

最小化测试

可以在加入项目前进行最小化测试是否连上redis:

python 复制代码
import asyncio
import redis.asyncio as redis


async def test_redis():
    print("🔵 开始测试 Redis 连接...")

    redis_client = redis.Redis(
        host="localhost",
        port=6379,
        db=1,
        decode_responses=True,
        protocol=2
    )

    try:
        await redis_client.ping()
        print("✅ Redis 连接成功")

        # 测试写入
        await redis_client.set("test_key", "test_value", ex=10)
        print("✅ 写入测试成功")

        # 测试读取
        value = await redis_client.get("test_key")
        print(f"✅ 读取测试成功: {value}")

    except Exception as e:
        print(f"❌ Redis 连接失败: {e}")
    finally:
        await redis_client.close()

# ✅ 调用函数
if __name__ == "__main__":
    print("🚀 开始执行测试...")
    asyncio.run(test_redis())
    print("🏁 测试完成")

部分内容参考相关教程,上述内容为个人整理实践总结而成,转载请注明出处!!!

相关推荐
●VON1 小时前
HarmonyKit | 鸿蒙开发:hvigor 构建系统命令行与缓存机制详解
缓存·华为·交互·harmonyos·鸿蒙
想会飞的蒲公英1 小时前
逻辑回归为什么叫回归,却用来做分类
人工智能·python·分类·回归·逻辑回归
数据知道1 小时前
DNS 安全攻防:缓存投毒、DNS 隧道与检测实战
安全·网络安全·缓存·缓存投毒
糖墨夕1 小时前
第一章:为什么你要学 AI Agent?—— 从"切图仔"到全栈的进化之路
前端·javascript·vue.js
随风一样自由1 小时前
【前端+Canvas+航天】长征十号乙火箭回收全流程动画
前端·canvas·长征十号乙
CoderYanger1 小时前
视频裁剪+缩放+自动添加水印脚本(Python版)
开发语言·后端·python·程序人生·职场和发展·音视频·学习方法
霍格沃兹测试开发学社测试人社区2 小时前
Node.js 浏览器引擎 + Python 大脑:Playwright 混合架构爬虫系统深度解析
python·架构·node.js
CappuccinoRose2 小时前
CSS、Less、Sass(含 SCSS)、Stylus
前端·css·less·sass·stylus
绘梨衣5472 小时前
求助帖:pdf复杂表格解析
爬虫·python·pdf