新闻头条后端:新闻缓存模块
缓存模块
数据缓存
缓存是一种存储机制,用于临时存储数据或计算结果,当再次需要这些数据时,可以快速从缓存中检索,而不是重新进行耗时或昂贵的获取和计算过程。在网站开发中,缓存(Cache)是一个非常重要的概念,其核心作用是提高性能、降低延迟和减轻服务器负载。
主要优势:
- 提升性能和用户体验
- 减轻服务器/数据库负载
- 降低网络延迟
- 节省资源和成本
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("🏁 测试完成")
部分内容参考相关教程,上述内容为个人整理实践总结而成,转载请注明出处!!!