redis_cache_service.py 16 KB

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