优化缓存机制,支持Redis和本地缓存的切换

This commit is contained in:
jxxghp
2025-08-20 09:16:30 +08:00
parent 89e8a64734
commit 69cb07c527
3 changed files with 256 additions and 162 deletions

View File

@@ -54,6 +54,7 @@ class ChainBase(metaclass=ABCMeta):
try:
self._redis_helper = RedisHelper(redis_url=settings.CACHE_BACKEND_URL)
except RuntimeError as e:
self._redis_helper = None
logger.warning(f"Redis缓存初始化失败将使用本地缓存: {e}")
def load_cache(self, filename: str) -> Any:

View File

@@ -9,9 +9,10 @@ from typing import Optional
from app.core.config import settings
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.helper.redis import RedisHelper
from app.log import logger
from app.utils.singleton import WeakSingleton
from app.schemas.types import MediaType
from app.utils.singleton import WeakSingleton
lock = RLock()
@@ -30,18 +31,35 @@ class DoubanCache(metaclass=WeakSingleton):
}
"""
# TMDB缓存过期
_tmdb_cache_expire: bool = True
_douban_cache_expire: bool = True
def __init__(self):
# 初始化Redis缓存助手
self._redis_helper = None
if settings.CACHE_BACKEND_TYPE == "redis":
try:
self._redis_helper = RedisHelper(redis_url=settings.CACHE_BACKEND_URL)
except RuntimeError as e:
logger.warning(f"豆瓣缓存Redis初始化失败将使用本地缓存: {e}")
self._redis_helper = None
# 加载本地缓存数据
self._meta_path = settings.TEMP_PATH / "__douban_cache__"
self._meta_data = self.__load(self._meta_path)
if not self._redis_helper:
self._meta_data = self.__load(self._meta_path)
def clear(self):
"""
清空所有TMDB缓存
清空所有豆瓣缓存
"""
with lock:
self._meta_data = {}
# 如果Redis可用同时清理Redis缓存
if self._redis_helper:
try:
self._redis_helper.clear(region="douban_cache")
logger.debug("已清理豆瓣Redis缓存")
except Exception as e:
logger.warning(f"清理豆瓣Redis缓存失败: {e}")
@staticmethod
def __get_key(meta: MetaBase) -> str:
@@ -56,16 +74,28 @@ class DoubanCache(metaclass=WeakSingleton):
根据KEY值获取缓存值
"""
key = self.__get_key(meta)
with lock:
info: dict = self._meta_data.get(key)
if info:
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR)
if not expire or int(time.time()) < expire:
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
self._meta_data[key] = info
elif expire and self._tmdb_cache_expire:
self.delete(key)
return info or {}
if self._redis_helper:
# 如果Redis可用从Redis读取
try:
redis_data = self._redis_helper.get(key, region="douban_cache")
return redis_data or {}
except Exception as e:
logger.warning(f"从Redis获取豆瓣缓存失败: {e}")
else:
# Redis不可用时从内存缓存读取
with lock:
info: dict = self._meta_data.get(key)
if info:
# 检查过期时间
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR)
if not expire or int(time.time()) < expire:
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
self._meta_data[key] = info
elif expire and self._douban_cache_expire:
self.delete(key)
return info or {}
return {}
def delete(self, key: str) -> dict:
"""
@@ -73,39 +103,44 @@ class DoubanCache(metaclass=WeakSingleton):
@param key: 缓存key
@return: 被删除的缓存内容
"""
with lock:
return self._meta_data.pop(key, {})
def delete_by_doubanid(self, doubanid: str) -> None:
"""
清空对应豆瓣ID的所有缓存记录以强制更新TMDB中最新的数据
"""
for key in list(self._meta_data):
if self._meta_data.get(key, {}).get("id") == doubanid:
with lock:
self._meta_data.pop(key)
def delete_unknown(self) -> None:
"""
清除未识别的缓存记录以便重新搜索TMDB
"""
for key in list(self._meta_data):
if self._meta_data.get(key, {}).get("id") == "0":
with lock:
self._meta_data.pop(key)
if self._redis_helper:
# 如果Redis可用删除Redis缓存
try:
self._redis_helper.delete(key, region="douban_cache")
return {}
except Exception as e:
logger.warning(f"删除豆瓣Redis缓存失败: {e}")
return {}
else:
# Redis不可用时删除内存缓存
with lock:
return self._meta_data.pop(key, {})
def modify(self, key: str, title: str) -> dict:
"""
删除缓存信息
修改缓存信息
@param key: 缓存key
@param title: 标题
@return: 被修改后缓存内容
"""
with lock:
if self._meta_data.get(key):
self._meta_data[key]['title'] = title
self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
return self._meta_data.get(key)
if self._redis_helper:
# 如果Redis可用修改Redis缓存
try:
redis_data = self._redis_helper.get(key, region="douban_cache")
if redis_data:
redis_data['title'] = title
self._redis_helper.set(key, redis_data, ttl=EXPIRE_TIMESTAMP, region="douban_cache")
return redis_data
except Exception as e:
logger.warning(f"修改豆瓣Redis缓存失败: {e}")
return {}
else:
# Redis不可用时修改内存缓存
with lock:
if self._meta_data.get(key):
self._meta_data[key]['title'] = title
self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
return self._meta_data.get(key)
@staticmethod
def __load(path: Path) -> dict:
@@ -126,31 +161,47 @@ class DoubanCache(metaclass=WeakSingleton):
"""
新增或更新缓存条目
"""
with lock:
if info:
# 缓存标题
cache_title = info.get("title")
# 缓存年份
cache_year = info.get('year')
# 类型
if isinstance(info.get('media_type'), MediaType):
mtype = info.get('media_type')
elif info.get("type"):
mtype = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
if info:
# 缓存标题
cache_title = info.get("title")
# 缓存年份
cache_year = info.get('year')
# 类型
if isinstance(info.get('media_type'), MediaType):
mtype = info.get('media_type')
elif info.get("type"):
mtype = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
else:
meta = MetaInfo(cache_title)
if meta.begin_season:
mtype = MediaType.TV
else:
meta = MetaInfo(cache_title)
if meta.begin_season:
mtype = MediaType.TV
else:
mtype = MediaType.MOVIE
# 海报
poster_path = info.get("pic", {}).get("large")
if not poster_path and info.get("cover_url"):
poster_path = info.get("cover_url")
if not poster_path and info.get("cover"):
poster_path = info.get("cover").get("url")
mtype = MediaType.MOVIE
# 海报
poster_path = info.get("pic", {}).get("large")
if not poster_path and info.get("cover_url"):
poster_path = info.get("cover_url")
if not poster_path and info.get("cover"):
poster_path = info.get("cover").get("url")
self._meta_data[self.__get_key(meta)] = {
if self._redis_helper:
# 如果Redis可用保存到Redis
cache_data = {
"id": info.get("id"),
"type": mtype,
"year": cache_year,
"title": cache_title,
"poster_path": poster_path
}
try:
self._redis_helper.set(self.__get_key(meta), cache_data, ttl=EXPIRE_TIMESTAMP,
region="douban_cache")
except Exception as e:
logger.warning(f"保存豆瓣缓存到Redis失败: {e}")
else:
# Redis不可用时保存到内存缓存
with lock:
cache_data = {
"id": info.get("id"),
"type": mtype,
"year": cache_year,
@@ -158,15 +209,29 @@ class DoubanCache(metaclass=WeakSingleton):
"poster_path": poster_path,
CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP
}
elif info is not None:
# None时不缓存此时代表网络错误允许重复请求
self._meta_data[self.__get_key(meta)] = {'id': "0"}
self._meta_data[self.__get_key(meta)] = cache_data
elif info is not None:
# None时不缓存此时代表网络错误允许重复请求
if self._redis_helper:
try:
self._redis_helper.set(self.__get_key(meta), {'id': "0"}, ttl=EXPIRE_TIMESTAMP,
region="douban_cache")
except Exception as e:
logger.warning(f"保存豆瓣缓存到Redis失败: {e}")
else:
with lock:
self._meta_data[self.__get_key(meta)] = {'id': "0"}
def save(self, force: Optional[bool] = False) -> None:
"""
保存缓存数据到文件
"""
# 如果Redis可用不需要保存到本地文件
if self._redis_helper:
return
# Redis不可用时保存到本地文件
meta_data = self.__load(self._meta_path)
new_meta_data = {k: v for k, v in self._meta_data.items() if v.get("id")}
@@ -176,7 +241,7 @@ class DoubanCache(metaclass=WeakSingleton):
return
with open(self._meta_path, 'wb') as f:
pickle.dump(new_meta_data, f, pickle.HIGHEST_PROTOCOL) # noqa
pickle.dump(new_meta_data, f, pickle.HIGHEST_PROTOCOL) # noqa
def _random_sample(self, new_meta_data: dict) -> bool:
"""
@@ -193,7 +258,7 @@ class DoubanCache(metaclass=WeakSingleton):
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
elif int(time.time()) >= expire:
ret = True
if self._tmdb_cache_expire:
if self._douban_cache_expire:
new_meta_data.pop(k)
else:
count = 0
@@ -206,30 +271,12 @@ class DoubanCache(metaclass=WeakSingleton):
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
elif int(time.time()) >= expire:
ret = True
if self._tmdb_cache_expire:
if self._douban_cache_expire:
new_meta_data.pop(k)
count += 1
if count >= 5:
ret |= self._random_sample(new_meta_data)
return ret
def get_title(self, key: str) -> Optional[str]:
"""
获取缓存的标题
"""
cache_media_info = self._meta_data.get(key)
if not cache_media_info or not cache_media_info.get("id"):
return None
return cache_media_info.get("title")
def set_title(self, key: str, cn_title: str) -> None:
"""
重新设置缓存标题
"""
cache_media_info = self._meta_data.get(key)
if not cache_media_info:
return
self._meta_data[key]['title'] = cn_title
def __del__(self):
self.save()

View File

@@ -4,13 +4,13 @@ import time
import traceback
from pathlib import Path
from threading import RLock
from typing import Optional
from app.core.config import settings
from app.core.meta import MetaBase
from app.helper.redis import RedisHelper
from app.log import logger
from app.utils.singleton import WeakSingleton
from app.schemas.types import MediaType
from app.utils.singleton import WeakSingleton
lock = RLock()
@@ -32,8 +32,19 @@ class TmdbCache(metaclass=WeakSingleton):
_tmdb_cache_expire: bool = True
def __init__(self):
# 初始化Redis缓存助手
self._redis_helper = None
if settings.CACHE_BACKEND_TYPE == "redis":
try:
self._redis_helper = RedisHelper(redis_url=settings.CACHE_BACKEND_URL)
except RuntimeError as e:
logger.warning(f"TMDB缓存Redis初始化失败将使用本地缓存: {e}")
self._redis_helper = None
# 加载缓存数据
self._meta_path = settings.TEMP_PATH / "__tmdb_cache__"
self._meta_data = self.__load(self._meta_path)
if not self._redis_helper:
self._meta_data = self.__load(self._meta_path)
def clear(self):
"""
@@ -41,6 +52,13 @@ class TmdbCache(metaclass=WeakSingleton):
"""
with lock:
self._meta_data = {}
# 如果Redis可用同时清理Redis缓存
if self._redis_helper:
try:
self._redis_helper.clear(region="tmdb_cache")
logger.debug("已清理TMDB Redis缓存")
except Exception as e:
logger.warning(f"清理TMDB Redis缓存失败: {e}")
@staticmethod
def __get_key(meta: MetaBase) -> str:
@@ -54,16 +72,28 @@ class TmdbCache(metaclass=WeakSingleton):
根据KEY值获取缓存值
"""
key = self.__get_key(meta)
with lock:
info: dict = self._meta_data.get(key)
if info:
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR)
if not expire or int(time.time()) < expire:
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
self._meta_data[key] = info
elif expire and self._tmdb_cache_expire:
self.delete(key)
return info or {}
if self._redis_helper:
# 如果Redis可用从Redis读取
try:
redis_data = self._redis_helper.get(key, region="tmdb_cache")
return redis_data or {}
except Exception as e:
logger.warning(f"从Redis获取TMDB缓存失败: {e}")
else:
# Redis不可用时从内存缓存读取
with lock:
info: dict = self._meta_data.get(key)
if info:
# 检查过期时间
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR)
if not expire or int(time.time()) < expire:
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
self._meta_data[key] = info
elif expire and self._tmdb_cache_expire:
self.delete(key)
return info or {}
return {}
def delete(self, key: str) -> dict:
"""
@@ -71,39 +101,44 @@ class TmdbCache(metaclass=WeakSingleton):
@param key: 缓存key
@return: 被删除的缓存内容
"""
with lock:
return self._meta_data.pop(key, {})
def delete_by_tmdbid(self, tmdbid: int) -> None:
"""
清空对应TMDBID的所有缓存记录以强制更新TMDB中最新的数据
"""
for key in list(self._meta_data):
if self._meta_data.get(key, {}).get("id") == tmdbid:
with lock:
self._meta_data.pop(key)
def delete_unknown(self) -> None:
"""
清除未识别的缓存记录以便重新搜索TMDB
"""
for key in list(self._meta_data):
if self._meta_data.get(key, {}).get("id") == 0:
with lock:
self._meta_data.pop(key)
if self._redis_helper:
# 如果Redis可用删除Redis缓存
try:
self._redis_helper.delete(key, region="tmdb_cache")
return {}
except Exception as e:
logger.warning(f"删除TMDB Redis缓存失败: {e}")
return {}
else:
# Redis不可用时删除内存缓存
with lock:
return self._meta_data.pop(key, {})
def modify(self, key: str, title: str) -> dict:
"""
删除缓存信息
修改缓存信息
@param key: 缓存key
@param title: 标题
@return: 被修改后缓存内容
"""
with lock:
if self._meta_data.get(key):
self._meta_data[key]['title'] = title
self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
return self._meta_data.get(key)
if self._redis_helper:
# 如果Redis可用修改Redis缓存
try:
redis_data = self._redis_helper.get(key, region="tmdb_cache")
if redis_data:
redis_data['title'] = title
self._redis_helper.set(key, redis_data, ttl=EXPIRE_TIMESTAMP, region="tmdb_cache")
return redis_data
except Exception as e:
logger.warning(f"修改TMDB Redis缓存失败: {e}")
return {}
else:
# Redis不可用时修改内存缓存
with lock:
if self._meta_data.get(key):
self._meta_data[key]['title'] = title
self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
return self._meta_data.get(key)
@staticmethod
def __load(path: Path) -> dict:
@@ -115,43 +150,72 @@ class TmdbCache(metaclass=WeakSingleton):
with open(path, 'rb') as f:
data = pickle.load(f)
return data
return {}
except Exception as e:
logger.error(f'加载缓存失败:{str(e)} - {traceback.format_exc()}')
return {}
return {}
def update(self, meta: MetaBase, info: dict) -> None:
"""
新增或更新缓存条目
"""
with lock:
if info:
# 缓存标题
cache_title = info.get("title") \
if info.get("media_type") == MediaType.MOVIE else info.get("name")
# 缓存年份
cache_year = info.get('release_date') \
if info.get("media_type") == MediaType.MOVIE else info.get('first_air_date')
if cache_year:
cache_year = cache_year[:4]
self._meta_data[self.__get_key(meta)] = {
if info:
# 缓存标题
cache_title = info.get("title") \
if info.get("media_type") == MediaType.MOVIE else info.get("name")
# 缓存年份
cache_year = info.get('release_date') \
if info.get("media_type") == MediaType.MOVIE else info.get('first_air_date')
if cache_year:
cache_year = cache_year[:4]
if self._redis_helper:
# 如果Redis可用保存到Redis
cache_data = {
"id": info.get("id"),
"type": info.get("media_type"),
"year": cache_year,
"title": cache_title,
"poster_path": info.get("poster_path"),
"backdrop_path": info.get("backdrop_path"),
CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP
"backdrop_path": info.get("backdrop_path")
}
elif info is not None:
# None时不缓存此时代表网络错误允许重复请求
self._meta_data[self.__get_key(meta)] = {'id': 0}
try:
self._redis_helper.set(self.__get_key(meta), cache_data, ttl=EXPIRE_TIMESTAMP, region="tmdb_cache")
except Exception as e:
logger.warning(f"保存TMDB缓存到Redis失败: {e}")
else:
# Redis不可用时保存到内存缓存
with lock:
cache_data = {
"id": info.get("id"),
"type": info.get("media_type"),
"year": cache_year,
"title": cache_title,
"poster_path": info.get("poster_path"),
"backdrop_path": info.get("backdrop_path"),
CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP
}
self._meta_data[self.__get_key(meta)] = cache_data
elif info is not None:
# None时不缓存此时代表网络错误允许重复请求
if self._redis_helper:
try:
self._redis_helper.set(self.__get_key(meta), {'id': 0}, ttl=EXPIRE_TIMESTAMP, region="tmdb_cache")
except Exception as e:
logger.warning(f"保存TMDB缓存到Redis失败: {e}")
else:
with lock:
self._meta_data[self.__get_key(meta)] = {'id': 0}
def save(self, force: bool = False) -> None:
"""
保存缓存数据到文件
"""
# 如果Redis可用不需要保存到本地文件
if self._redis_helper:
return
# Redis不可用时保存到本地文件
meta_data = self.__load(self._meta_path)
new_meta_data = {k: v for k, v in self._meta_data.items() if v.get("id")}
@@ -198,23 +262,5 @@ class TmdbCache(metaclass=WeakSingleton):
ret |= self._random_sample(new_meta_data)
return ret
def get_title(self, key: str) -> Optional[str]:
"""
获取缓存的标题
"""
cache_media_info = self._meta_data.get(key)
if not cache_media_info or not cache_media_info.get("id"):
return None
return cache_media_info.get("title")
def set_title(self, key: str, cn_title: str) -> None:
"""
重新设置缓存标题
"""
cache_media_info = self._meta_data.get(key)
if not cache_media_info:
return
self._meta_data[key]['title'] = cn_title
def __del__(self):
self.save()