redis_cache_service.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. """
  2. Redis缓存管理服务
  3. 用于高速缓存对话上下文,减少MySQL查询
  4. """
  5. import json
  6. import time
  7. from typing import Awaitable, Callable, Dict, List, Optional
  8. import logging
  9. import redis.asyncio as aioredis
  10. logger = logging.getLogger(__name__)
  11. class RedisCacheService:
  12. """Redis缓存服务"""
  13. def __init__(self, redis_provider: Callable[[], Awaitable[aioredis.Redis]]):
  14. """
  15. 初始化Redis缓存服务
  16. Args:
  17. redis_provider: 一个可调用对象,返回 redis.asyncio.Redis 实例
  18. """
  19. if not callable(redis_provider):
  20. raise ValueError("redis_provider 必须可调用")
  21. self._redis_provider = redis_provider
  22. # 缓存Key设计
  23. self.KEY_USER_CONTEXT = "context:{user_id}" # 用户上下文
  24. self.KEY_USER_MESSAGES = "messages:{user_id}" # 用户消息列表
  25. self.KEY_USER_ROUND = "round:{user_id}" # 当前轮次
  26. self.KEY_USER_SUMMARY = "summary:{user_id}:{end_round}" # 历史总结
  27. # TTL设置
  28. self.TTL_CONTEXT = 86400 # 24小时
  29. self.TTL_MESSAGES = 86400 # 24小时
  30. self.TTL_SUMMARY = 604800 # 7天
  31. async def _get_redis(self) -> aioredis.Redis:
  32. """获取最新的Redis客户端,确保连接有效"""
  33. redis_client = await self._redis_provider()
  34. if not redis_client:
  35. raise RuntimeError("Redis 客户端不可用")
  36. return redis_client
  37. async def get_context(self, user_id: str) -> Optional[Dict]:
  38. """
  39. 从Redis获取用户上下文(零延迟)
  40. Returns:
  41. {
  42. "summary": str | None, # 最新的一个总结(已包含所有历史信息)
  43. "recent_messages": List[Dict],
  44. "current_round": int,
  45. "total_tokens": int
  46. }
  47. """
  48. try:
  49. redis = await self._get_redis()
  50. key = self.KEY_USER_CONTEXT.format(user_id=user_id)
  51. cached = await redis.get(key)
  52. if cached:
  53. context = json.loads(cached)
  54. logger.info(f"⚡ Redis命中: user={user_id}, round={context.get('current_round')}")
  55. return context
  56. logger.info(f"⚠️ Redis未命中: user={user_id}")
  57. return None
  58. except Exception as e:
  59. logger.error(f"❌ Redis读取失败: {e}")
  60. return None
  61. async def set_context(
  62. self,
  63. user_id: str,
  64. context: Dict,
  65. ttl: int = None
  66. ) -> bool:
  67. """
  68. 更新Redis上下文缓存(超快写入)
  69. Args:
  70. user_id: 用户ID
  71. context: 上下文数据
  72. ttl: 过期时间(秒),默认24小时
  73. """
  74. try:
  75. redis = await self._get_redis()
  76. key = self.KEY_USER_CONTEXT.format(user_id=user_id)
  77. ttl = ttl or self.TTL_CONTEXT
  78. await redis.setex(
  79. key,
  80. ttl,
  81. json.dumps(context, ensure_ascii=False)
  82. )
  83. logger.info(f"✅ Redis更新: user={user_id}")
  84. return True
  85. except Exception as e:
  86. logger.error(f"❌ Redis写入失败: {e}")
  87. return False
  88. async def append_message(
  89. self,
  90. user_id: str,
  91. role: str,
  92. content: str,
  93. round_number: int
  94. ) -> bool:
  95. """
  96. 追加消息到Redis(实时更新)
  97. 使用Redis List结构,支持高效追加和范围查询
  98. """
  99. try:
  100. redis = await self._get_redis()
  101. key = self.KEY_USER_MESSAGES.format(user_id=user_id)
  102. message = {
  103. 'role': role,
  104. 'content': content,
  105. 'round_number': round_number,
  106. 'timestamp': time.time()
  107. }
  108. # 追加到List尾部
  109. await redis.rpush(key, json.dumps(message, ensure_ascii=False))
  110. # 设置过期时间
  111. await redis.expire(key, self.TTL_MESSAGES)
  112. # 限制List长度(最多保留200条消息)
  113. await redis.ltrim(key, -200, -1)
  114. logger.debug(f"✅ Redis追加消息: user={user_id}, round={round_number}, role={role}")
  115. return True
  116. except Exception as e:
  117. logger.error(f"❌ Redis追加失败: {e}")
  118. return False
  119. async def get_recent_messages(
  120. self,
  121. user_id: str,
  122. count: int = 30
  123. ) -> List[Dict]:
  124. """
  125. 获取最近N条消息(从Redis List)
  126. Args:
  127. user_id: 用户ID
  128. count: 消息数量(默认30条,即15轮)
  129. """
  130. try:
  131. redis = await self._get_redis()
  132. key = self.KEY_USER_MESSAGES.format(user_id=user_id)
  133. # 获取最后N条消息
  134. messages_raw = await redis.lrange(key, -count, -1)
  135. messages = []
  136. for msg_bytes in messages_raw:
  137. try:
  138. # 解码并解析JSON
  139. msg_str = msg_bytes.decode('utf-8') if isinstance(msg_bytes, bytes) else msg_bytes
  140. messages.append(json.loads(msg_str))
  141. except Exception as e:
  142. logger.warning(f"⚠️ 解析消息失败: {e}")
  143. continue
  144. logger.debug(f"⚡ Redis获取消息: user={user_id}, count={len(messages)}")
  145. return messages
  146. except Exception as e:
  147. logger.error(f"❌ Redis读取消息失败: {e}")
  148. return []
  149. async def get_current_round(self, user_id: str) -> int:
  150. """获取当前轮次"""
  151. try:
  152. redis = await self._get_redis()
  153. key = self.KEY_USER_ROUND.format(user_id=user_id)
  154. round_bytes = await redis.get(key)
  155. if round_bytes:
  156. round_str = round_bytes.decode('utf-8') if isinstance(round_bytes, bytes) else round_bytes
  157. return int(round_str)
  158. return 0
  159. except Exception as e:
  160. logger.error(f"❌ Redis读取轮次失败: {e}")
  161. return 0
  162. async def set_current_round(self, user_id: str, round_number: int) -> bool:
  163. """设置当前轮次"""
  164. try:
  165. redis = await self._get_redis()
  166. key = self.KEY_USER_ROUND.format(user_id=user_id)
  167. await redis.setex(key, self.TTL_CONTEXT, str(round_number))
  168. return True
  169. except Exception as e:
  170. logger.error(f"❌ Redis设置轮次失败: {e}")
  171. return False
  172. async def increment_round(self, user_id: str) -> int:
  173. """轮次自增"""
  174. try:
  175. redis = await self._get_redis()
  176. key = self.KEY_USER_ROUND.format(user_id=user_id)
  177. new_round = await redis.incr(key)
  178. await redis.expire(key, self.TTL_CONTEXT)
  179. # 同时更新 user:{user_id}:meta 中的 current_round(保持一致性)
  180. meta_key = f"user:{user_id}:meta"
  181. await redis.hset(meta_key, 'current_round', new_round)
  182. await redis.expire(meta_key, self.TTL_CONTEXT)
  183. logger.debug(f"✅ Redis轮次自增: user={user_id}, new_round={new_round}")
  184. return new_round
  185. except Exception as e:
  186. logger.error(f"❌ Redis轮次自增失败: {e}")
  187. return 0
  188. async def get_summary(self, user_id: str, end_round: int) -> Optional[str]:
  189. """获取历史总结"""
  190. try:
  191. redis = await self._get_redis()
  192. key = self.KEY_USER_SUMMARY.format(user_id=user_id, end_round=end_round)
  193. summary_bytes = await redis.get(key)
  194. if summary_bytes:
  195. summary = summary_bytes.decode('utf-8') if isinstance(summary_bytes, bytes) else summary_bytes
  196. logger.info(f"⚡ Redis获取总结: user={user_id}, end_round={end_round}")
  197. return summary
  198. return None
  199. except Exception as e:
  200. logger.error(f"❌ Redis读取总结失败: {e}")
  201. return None
  202. async def set_summary(
  203. self,
  204. user_id: str,
  205. end_round: int,
  206. summary: str
  207. ) -> bool:
  208. """保存历史总结"""
  209. try:
  210. redis = await self._get_redis()
  211. key = self.KEY_USER_SUMMARY.format(user_id=user_id, end_round=end_round)
  212. await redis.setex(key, self.TTL_SUMMARY, summary)
  213. logger.info(f"✅ Redis保存总结: user={user_id}, end_round={end_round}, 长度={len(summary)}字")
  214. return True
  215. except Exception as e:
  216. logger.error(f"❌ Redis保存总结失败: {e}")
  217. return False
  218. async def build_context_from_cache(
  219. self,
  220. user_id: str,
  221. current_round: int,
  222. db_manager=None
  223. ) -> Dict:
  224. """
  225. 从Redis构建完整上下文(超快)
  226. 逻辑:
  227. - 如果 current_round <= 15: 返回所有消息
  228. - 如果 current_round > 15: 返回所有历史总结 + 最近15轮
  229. Args:
  230. user_id: 用户ID
  231. current_round: 当前轮次
  232. db_manager: 数据库管理器(用于获取所有总结)
  233. """
  234. try:
  235. if current_round <= 15:
  236. # 前15轮:直接返回所有消息
  237. messages = await self.get_recent_messages(user_id, count=current_round * 2)
  238. return {
  239. "summary": None,
  240. "recent_messages": messages,
  241. "current_round": current_round,
  242. "total_tokens": self._estimate_tokens(messages=messages)
  243. }
  244. else:
  245. # 超过15轮:最新总结 + 最近15轮
  246. # 🔥 修复:查找 end_round < current_round 的最新总结
  247. # 因为总结是累积的,最新的总结包含了所有历史信息
  248. summary_end_round = current_round - 1
  249. logger.info(f"🔍 [Redis] 查询总结: user={user_id}, current_round={current_round}, summary_end_round={summary_end_round}")
  250. # 获取消息
  251. messages = await self.get_recent_messages(user_id, count=30) # 15轮=30条消息
  252. # 从数据库获取最新的一个总结(已包含所有历史信息,使用 ORM)
  253. summary = None
  254. if db_manager:
  255. try:
  256. from models_db import ConversationSummary
  257. from sqlalchemy import select, func
  258. async with db_manager.get_session() as session:
  259. # 先查询所有总结,用于调试
  260. all_summaries_result = await session.execute(
  261. select(
  262. ConversationSummary.end_round,
  263. ConversationSummary.start_round,
  264. ConversationSummary.created_at
  265. )
  266. .where(ConversationSummary.user_id == user_id)
  267. .order_by(ConversationSummary.end_round.desc())
  268. .limit(10)
  269. )
  270. all_summaries = [
  271. {
  272. 'end_round': row.end_round,
  273. 'start_round': row.start_round,
  274. 'created_at': row.created_at
  275. }
  276. for row in all_summaries_result.all()
  277. ]
  278. if all_summaries:
  279. logger.info(f"📊 [Redis] 数据库中的总结: user={user_id}, 总数={len(all_summaries)}, 列表={[(s['start_round'], s['end_round']) for s in all_summaries]}")
  280. else:
  281. logger.warning(f"⚠️ [Redis] 数据库中没有任何总结: user={user_id}")
  282. # 调试:查询数据库中所有不同的 user_id
  283. try:
  284. all_user_ids_result = await session.execute(
  285. select(
  286. ConversationSummary.user_id,
  287. func.count(ConversationSummary.id).label('summary_count')
  288. )
  289. .group_by(ConversationSummary.user_id)
  290. .limit(10)
  291. )
  292. all_user_ids = [
  293. {
  294. 'user_id': row.user_id,
  295. 'summary_count': row.summary_count
  296. }
  297. for row in all_user_ids_result.all()
  298. ]
  299. if all_user_ids:
  300. logger.info(f"🔍 [Redis] 数据库中存在的其他 user_id: {[(u['user_id'], u['summary_count']) for u in all_user_ids]}")
  301. except Exception as e:
  302. logger.debug(f"查询所有 user_id 失败: {e}")
  303. # 查询符合条件的总结
  304. summary_result = await session.execute(
  305. select(
  306. ConversationSummary.summary_text,
  307. ConversationSummary.end_round
  308. )
  309. .where(
  310. ConversationSummary.user_id == user_id,
  311. ConversationSummary.end_round <= summary_end_round
  312. )
  313. .order_by(ConversationSummary.end_round.desc())
  314. .limit(1)
  315. )
  316. summary_row = summary_result.first()
  317. if summary_row:
  318. summary = summary_row.summary_text
  319. end_round = summary_row.end_round
  320. logger.info(f"✅ [Redis] 获取最新总结: user={user_id}, end_round={end_round}, summary_length={len(summary)}")
  321. else:
  322. logger.warning(f"⚠️ [Redis] 未找到总结: user={user_id}, up_to_round={summary_end_round}, 数据库中总结数={len(all_summaries) if all_summaries else 0}")
  323. except Exception as e:
  324. logger.warning(f"⚠️ 从数据库获取总结失败: {e}")
  325. import traceback
  326. logger.error(traceback.format_exc())
  327. return {
  328. "summary": summary,
  329. "recent_messages": messages,
  330. "current_round": current_round,
  331. "total_tokens": self._estimate_tokens(summary=summary, messages=messages)
  332. }
  333. except Exception as e:
  334. logger.error(f"❌ 从Redis构建上下文失败: {e}")
  335. return {
  336. "summary": None,
  337. "recent_messages": [],
  338. "current_round": current_round,
  339. "total_tokens": 0
  340. }
  341. def _estimate_tokens(self, summary: str = None, messages: List[Dict] = None) -> int:
  342. """估算token数量"""
  343. total = 0
  344. if summary:
  345. total += len(summary) // 2
  346. if messages:
  347. for msg in messages:
  348. total += len(msg.get('content', '')) // 2
  349. return total
  350. async def invalidate_context(self, user_id: str) -> bool:
  351. """清除用户上下文缓存(强制刷新)"""
  352. try:
  353. redis = await self._get_redis()
  354. key = self.KEY_USER_CONTEXT.format(user_id=user_id)
  355. await redis.delete(key)
  356. logger.info(f"🗑️ Redis清除缓存: user={user_id}")
  357. return True
  358. except Exception as e:
  359. logger.error(f"❌ Redis清除失败: {e}")
  360. return False
  361. async def sync_from_db(self, user_id: str, messages: List[Dict], current_round: int) -> bool:
  362. """
  363. 从数据库同步数据到Redis(初始化缓存)
  364. Args:
  365. user_id: 用户ID
  366. messages: 消息列表
  367. current_round: 当前轮次
  368. """
  369. try:
  370. redis = await self._get_redis()
  371. # 清空旧数据
  372. messages_key = self.KEY_USER_MESSAGES.format(user_id=user_id)
  373. await redis.delete(messages_key)
  374. # 批量写入消息
  375. if messages:
  376. pipeline = redis.pipeline()
  377. for msg in messages:
  378. pipeline.rpush(
  379. messages_key,
  380. json.dumps(msg, ensure_ascii=False)
  381. )
  382. pipeline.expire(messages_key, self.TTL_MESSAGES)
  383. await pipeline.execute()
  384. # 设置轮次
  385. await self.set_current_round(user_id, current_round)
  386. logger.info(f"✅ Redis同步完成: user={user_id}, messages={len(messages)}, round={current_round}")
  387. return True
  388. except Exception as e:
  389. logger.error(f"❌ Redis同步失败: {e}")
  390. return False
  391. async def health_check(self) -> bool:
  392. """健康检查"""
  393. try:
  394. redis = await self._get_redis()
  395. await redis.ping()
  396. return True
  397. except Exception as e:
  398. logger.error(f"❌ Redis健康检查失败: {e}")
  399. return False