优化缓存机制,支持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
+1
View File
@@ -54,6 +54,7 @@ class ChainBase(metaclass=ABCMeta):
try: try:
self._redis_helper = RedisHelper(redis_url=settings.CACHE_BACKEND_URL) self._redis_helper = RedisHelper(redis_url=settings.CACHE_BACKEND_URL)
except RuntimeError as e: except RuntimeError as e:
self._redis_helper = None
logger.warning(f"Redis缓存初始化失败,将使用本地缓存: {e}") logger.warning(f"Redis缓存初始化失败,将使用本地缓存: {e}")
def load_cache(self, filename: str) -> Any: def load_cache(self, filename: str) -> Any:
+134 -87
View File
@@ -9,9 +9,10 @@ from typing import Optional
from app.core.config import settings from app.core.config import settings
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.helper.redis import RedisHelper
from app.log import logger from app.log import logger
from app.utils.singleton import WeakSingleton
from app.schemas.types import MediaType from app.schemas.types import MediaType
from app.utils.singleton import WeakSingleton
lock = RLock() lock = RLock()
@@ -30,18 +31,35 @@ class DoubanCache(metaclass=WeakSingleton):
} }
""" """
# TMDB缓存过期 # TMDB缓存过期
_tmdb_cache_expire: bool = True _douban_cache_expire: bool = True
def __init__(self): 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_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): def clear(self):
""" """
清空所有TMDB缓存 清空所有豆瓣缓存
""" """
with lock: with lock:
self._meta_data = {} 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 @staticmethod
def __get_key(meta: MetaBase) -> str: def __get_key(meta: MetaBase) -> str:
@@ -56,16 +74,28 @@ class DoubanCache(metaclass=WeakSingleton):
根据KEY值获取缓存值 根据KEY值获取缓存值
""" """
key = self.__get_key(meta) key = self.__get_key(meta)
with lock:
info: dict = self._meta_data.get(key) if self._redis_helper:
if info: # 如果Redis可用,从Redis读取
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR) try:
if not expire or int(time.time()) < expire: redis_data = self._redis_helper.get(key, region="douban_cache")
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP return redis_data or {}
self._meta_data[key] = info except Exception as e:
elif expire and self._tmdb_cache_expire: logger.warning(f"从Redis获取豆瓣缓存失败: {e}")
self.delete(key) else:
return info or {} # 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: def delete(self, key: str) -> dict:
""" """
@@ -73,39 +103,44 @@ class DoubanCache(metaclass=WeakSingleton):
@param key: 缓存key @param key: 缓存key
@return: 被删除的缓存内容 @return: 被删除的缓存内容
""" """
with lock: if self._redis_helper:
return self._meta_data.pop(key, {}) # 如果Redis可用,删除Redis缓存
try:
def delete_by_doubanid(self, doubanid: str) -> None: self._redis_helper.delete(key, region="douban_cache")
""" return {}
清空对应豆瓣ID的所有缓存记录,以强制更新TMDB中最新的数据 except Exception as e:
""" logger.warning(f"删除豆瓣Redis缓存失败: {e}")
for key in list(self._meta_data): return {}
if self._meta_data.get(key, {}).get("id") == doubanid: else:
with lock: # Redis不可用时,删除内存缓存
self._meta_data.pop(key) with lock:
return 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)
def modify(self, key: str, title: str) -> dict: def modify(self, key: str, title: str) -> dict:
""" """
删除缓存信息 修改缓存信息
@param key: 缓存key @param key: 缓存key
@param title: 标题 @param title: 标题
@return: 被修改后缓存内容 @return: 被修改后缓存内容
""" """
with lock: if self._redis_helper:
if self._meta_data.get(key): # 如果Redis可用,修改Redis缓存
self._meta_data[key]['title'] = title try:
self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP redis_data = self._redis_helper.get(key, region="douban_cache")
return self._meta_data.get(key) 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 @staticmethod
def __load(path: Path) -> dict: def __load(path: Path) -> dict:
@@ -126,31 +161,47 @@ class DoubanCache(metaclass=WeakSingleton):
""" """
新增或更新缓存条目 新增或更新缓存条目
""" """
with lock: if info:
if info: # 缓存标题
# 缓存标题 cache_title = info.get("title")
cache_title = info.get("title") # 缓存年份
# 缓存年份 cache_year = info.get('year')
cache_year = info.get('year') # 类型
# 类型 if isinstance(info.get('media_type'), MediaType):
if isinstance(info.get('media_type'), MediaType): mtype = info.get('media_type')
mtype = info.get('media_type') elif info.get("type"):
elif info.get("type"): mtype = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV
mtype = MediaType.MOVIE if info.get("type") == "movie" else MediaType.TV else:
meta = MetaInfo(cache_title)
if meta.begin_season:
mtype = MediaType.TV
else: else:
meta = MetaInfo(cache_title) mtype = MediaType.MOVIE
if meta.begin_season: # 海报
mtype = MediaType.TV poster_path = info.get("pic", {}).get("large")
else: if not poster_path and info.get("cover_url"):
mtype = MediaType.MOVIE poster_path = info.get("cover_url")
# 海报 if not poster_path and info.get("cover"):
poster_path = info.get("pic", {}).get("large") poster_path = info.get("cover").get("url")
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"), "id": info.get("id"),
"type": mtype, "type": mtype,
"year": cache_year, "year": cache_year,
@@ -158,15 +209,29 @@ class DoubanCache(metaclass=WeakSingleton):
"poster_path": poster_path, "poster_path": poster_path,
CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP
} }
elif info is not None: self._meta_data[self.__get_key(meta)] = cache_data
# None时不缓存,此时代表网络错误,允许重复请求
self._meta_data[self.__get_key(meta)] = {'id': "0"} 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: def save(self, force: Optional[bool] = False) -> None:
""" """
保存缓存数据到文件 保存缓存数据到文件
""" """
# 如果Redis可用,不需要保存到本地文件
if self._redis_helper:
return
# Redis不可用时,保存到本地文件
meta_data = self.__load(self._meta_path) meta_data = self.__load(self._meta_path)
new_meta_data = {k: v for k, v in self._meta_data.items() if v.get("id")} 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 return
with open(self._meta_path, 'wb') as f: 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: 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 info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
elif int(time.time()) >= expire: elif int(time.time()) >= expire:
ret = True ret = True
if self._tmdb_cache_expire: if self._douban_cache_expire:
new_meta_data.pop(k) new_meta_data.pop(k)
else: else:
count = 0 count = 0
@@ -206,30 +271,12 @@ class DoubanCache(metaclass=WeakSingleton):
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP
elif int(time.time()) >= expire: elif int(time.time()) >= expire:
ret = True ret = True
if self._tmdb_cache_expire: if self._douban_cache_expire:
new_meta_data.pop(k) new_meta_data.pop(k)
count += 1 count += 1
if count >= 5: if count >= 5:
ret |= self._random_sample(new_meta_data) ret |= self._random_sample(new_meta_data)
return ret 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): def __del__(self):
self.save() self.save()
+121 -75
View File
@@ -4,13 +4,13 @@ import time
import traceback import traceback
from pathlib import Path from pathlib import Path
from threading import RLock from threading import RLock
from typing import Optional
from app.core.config import settings from app.core.config import settings
from app.core.meta import MetaBase from app.core.meta import MetaBase
from app.helper.redis import RedisHelper
from app.log import logger from app.log import logger
from app.utils.singleton import WeakSingleton
from app.schemas.types import MediaType from app.schemas.types import MediaType
from app.utils.singleton import WeakSingleton
lock = RLock() lock = RLock()
@@ -32,8 +32,19 @@ class TmdbCache(metaclass=WeakSingleton):
_tmdb_cache_expire: bool = True _tmdb_cache_expire: bool = True
def __init__(self): 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_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): def clear(self):
""" """
@@ -41,6 +52,13 @@ class TmdbCache(metaclass=WeakSingleton):
""" """
with lock: with lock:
self._meta_data = {} 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 @staticmethod
def __get_key(meta: MetaBase) -> str: def __get_key(meta: MetaBase) -> str:
@@ -54,16 +72,28 @@ class TmdbCache(metaclass=WeakSingleton):
根据KEY值获取缓存值 根据KEY值获取缓存值
""" """
key = self.__get_key(meta) key = self.__get_key(meta)
with lock:
info: dict = self._meta_data.get(key) if self._redis_helper:
if info: # 如果Redis可用,从Redis读取
expire = info.get(CACHE_EXPIRE_TIMESTAMP_STR) try:
if not expire or int(time.time()) < expire: redis_data = self._redis_helper.get(key, region="tmdb_cache")
info[CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP return redis_data or {}
self._meta_data[key] = info except Exception as e:
elif expire and self._tmdb_cache_expire: logger.warning(f"从Redis获取TMDB缓存失败: {e}")
self.delete(key) else:
return info or {} # 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: def delete(self, key: str) -> dict:
""" """
@@ -71,39 +101,44 @@ class TmdbCache(metaclass=WeakSingleton):
@param key: 缓存key @param key: 缓存key
@return: 被删除的缓存内容 @return: 被删除的缓存内容
""" """
with lock: if self._redis_helper:
return self._meta_data.pop(key, {}) # 如果Redis可用,删除Redis缓存
try:
def delete_by_tmdbid(self, tmdbid: int) -> None: self._redis_helper.delete(key, region="tmdb_cache")
""" return {}
清空对应TMDBID的所有缓存记录,以强制更新TMDB中最新的数据 except Exception as e:
""" logger.warning(f"删除TMDB Redis缓存失败: {e}")
for key in list(self._meta_data): return {}
if self._meta_data.get(key, {}).get("id") == tmdbid: else:
with lock: # Redis不可用时,删除内存缓存
self._meta_data.pop(key) with lock:
return 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)
def modify(self, key: str, title: str) -> dict: def modify(self, key: str, title: str) -> dict:
""" """
删除缓存信息 修改缓存信息
@param key: 缓存key @param key: 缓存key
@param title: 标题 @param title: 标题
@return: 被修改后缓存内容 @return: 被修改后缓存内容
""" """
with lock: if self._redis_helper:
if self._meta_data.get(key): # 如果Redis可用,修改Redis缓存
self._meta_data[key]['title'] = title try:
self._meta_data[key][CACHE_EXPIRE_TIMESTAMP_STR] = int(time.time()) + EXPIRE_TIMESTAMP redis_data = self._redis_helper.get(key, region="tmdb_cache")
return self._meta_data.get(key) 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 @staticmethod
def __load(path: Path) -> dict: def __load(path: Path) -> dict:
@@ -115,43 +150,72 @@ class TmdbCache(metaclass=WeakSingleton):
with open(path, 'rb') as f: with open(path, 'rb') as f:
data = pickle.load(f) data = pickle.load(f)
return data return data
return {}
except Exception as e: except Exception as e:
logger.error(f'加载缓存失败:{str(e)} - {traceback.format_exc()}') logger.error(f'加载缓存失败:{str(e)} - {traceback.format_exc()}')
return {} return {}
def update(self, meta: MetaBase, info: dict) -> None: def update(self, meta: MetaBase, info: dict) -> None:
""" """
新增或更新缓存条目 新增或更新缓存条目
""" """
with lock: if info:
if info: # 缓存标题
# 缓存标题 cache_title = info.get("title") \
cache_title = info.get("title") \ if info.get("media_type") == MediaType.MOVIE else info.get("name")
if info.get("media_type") == MediaType.MOVIE else info.get("name") # 缓存年份
# 缓存年份 cache_year = info.get('release_date') \
cache_year = info.get('release_date') \ if info.get("media_type") == MediaType.MOVIE else info.get('first_air_date')
if info.get("media_type") == MediaType.MOVIE else info.get('first_air_date') if cache_year:
if cache_year: cache_year = cache_year[:4]
cache_year = cache_year[:4]
self._meta_data[self.__get_key(meta)] = { if self._redis_helper:
# 如果Redis可用,保存到Redis
cache_data = {
"id": info.get("id"), "id": info.get("id"),
"type": info.get("media_type"), "type": info.get("media_type"),
"year": cache_year, "year": cache_year,
"title": cache_title, "title": cache_title,
"poster_path": info.get("poster_path"), "poster_path": info.get("poster_path"),
"backdrop_path": info.get("backdrop_path"), "backdrop_path": info.get("backdrop_path")
CACHE_EXPIRE_TIMESTAMP_STR: int(time.time()) + EXPIRE_TIMESTAMP
} }
elif info is not None: try:
# None时不缓存,此时代表网络错误,允许重复请求 self._redis_helper.set(self.__get_key(meta), cache_data, ttl=EXPIRE_TIMESTAMP, region="tmdb_cache")
self._meta_data[self.__get_key(meta)] = {'id': 0} 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: def save(self, force: bool = False) -> None:
""" """
保存缓存数据到文件 保存缓存数据到文件
""" """
# 如果Redis可用,不需要保存到本地文件
if self._redis_helper:
return
# Redis不可用时,保存到本地文件
meta_data = self.__load(self._meta_path) meta_data = self.__load(self._meta_path)
new_meta_data = {k: v for k, v in self._meta_data.items() if v.get("id")} 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) ret |= self._random_sample(new_meta_data)
return ret 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): def __del__(self):
self.save() self.save()