| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469 |
- """
- Redis缓存管理服务
- 用于高速缓存对话上下文,减少MySQL查询
- """
- import json
- import time
- from typing import Awaitable, Callable, Dict, List, Optional
- import logging
- import redis.asyncio as aioredis
- logger = logging.getLogger(__name__)
- class RedisCacheService:
- """Redis缓存服务"""
-
- def __init__(self, redis_provider: Callable[[], Awaitable[aioredis.Redis]]):
- """
- 初始化Redis缓存服务
-
- Args:
- redis_provider: 一个可调用对象,返回 redis.asyncio.Redis 实例
- """
- if not callable(redis_provider):
- raise ValueError("redis_provider 必须可调用")
- self._redis_provider = redis_provider
-
- # 缓存Key设计
- self.KEY_USER_CONTEXT = "context:{user_id}" # 用户上下文
- self.KEY_USER_MESSAGES = "messages:{user_id}" # 用户消息列表
- self.KEY_USER_ROUND = "round:{user_id}" # 当前轮次
- self.KEY_USER_SUMMARY = "summary:{user_id}:{end_round}" # 历史总结
-
- # TTL设置
- self.TTL_CONTEXT = 86400 # 24小时
- self.TTL_MESSAGES = 86400 # 24小时
- self.TTL_SUMMARY = 604800 # 7天
-
- async def _get_redis(self) -> aioredis.Redis:
- """获取最新的Redis客户端,确保连接有效"""
- redis_client = await self._redis_provider()
- if not redis_client:
- raise RuntimeError("Redis 客户端不可用")
- return redis_client
-
- async def get_context(self, user_id: str) -> Optional[Dict]:
- """
- 从Redis获取用户上下文(零延迟)
-
- Returns:
- {
- "summary": str | None, # 最新的一个总结(已包含所有历史信息)
- "recent_messages": List[Dict],
- "current_round": int,
- "total_tokens": int
- }
- """
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_CONTEXT.format(user_id=user_id)
- cached = await redis.get(key)
-
- if cached:
- context = json.loads(cached)
- logger.info(f"⚡ Redis命中: user={user_id}, round={context.get('current_round')}")
- return context
-
- logger.info(f"⚠️ Redis未命中: user={user_id}")
- return None
-
- except Exception as e:
- logger.error(f"❌ Redis读取失败: {e}")
- return None
-
- async def set_context(
- self,
- user_id: str,
- context: Dict,
- ttl: int = None
- ) -> bool:
- """
- 更新Redis上下文缓存(超快写入)
-
- Args:
- user_id: 用户ID
- context: 上下文数据
- ttl: 过期时间(秒),默认24小时
- """
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_CONTEXT.format(user_id=user_id)
- ttl = ttl or self.TTL_CONTEXT
-
- await redis.setex(
- key,
- ttl,
- json.dumps(context, ensure_ascii=False)
- )
-
- logger.info(f"✅ Redis更新: user={user_id}")
- return True
-
- except Exception as e:
- logger.error(f"❌ Redis写入失败: {e}")
- return False
-
- async def append_message(
- self,
- user_id: str,
- role: str,
- content: str,
- round_number: int
- ) -> bool:
- """
- 追加消息到Redis(实时更新)
-
- 使用Redis List结构,支持高效追加和范围查询
- """
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_MESSAGES.format(user_id=user_id)
-
- message = {
- 'role': role,
- 'content': content,
- 'round_number': round_number,
- 'timestamp': time.time()
- }
-
- # 追加到List尾部
- await redis.rpush(key, json.dumps(message, ensure_ascii=False))
-
- # 设置过期时间
- await redis.expire(key, self.TTL_MESSAGES)
-
- # 限制List长度(最多保留200条消息)
- await redis.ltrim(key, -200, -1)
-
- logger.debug(f"✅ Redis追加消息: user={user_id}, round={round_number}, role={role}")
- return True
-
- except Exception as e:
- logger.error(f"❌ Redis追加失败: {e}")
- return False
-
- async def get_recent_messages(
- self,
- user_id: str,
- count: int = 30
- ) -> List[Dict]:
- """
- 获取最近N条消息(从Redis List)
-
- Args:
- user_id: 用户ID
- count: 消息数量(默认30条,即15轮)
- """
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_MESSAGES.format(user_id=user_id)
-
- # 获取最后N条消息
- messages_raw = await redis.lrange(key, -count, -1)
-
- messages = []
- for msg_bytes in messages_raw:
- try:
- # 解码并解析JSON
- msg_str = msg_bytes.decode('utf-8') if isinstance(msg_bytes, bytes) else msg_bytes
- messages.append(json.loads(msg_str))
- except Exception as e:
- logger.warning(f"⚠️ 解析消息失败: {e}")
- continue
-
- logger.debug(f"⚡ Redis获取消息: user={user_id}, count={len(messages)}")
- return messages
-
- except Exception as e:
- logger.error(f"❌ Redis读取消息失败: {e}")
- return []
-
- async def get_current_round(self, user_id: str) -> int:
- """获取当前轮次"""
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_ROUND.format(user_id=user_id)
- round_bytes = await redis.get(key)
-
- if round_bytes:
- round_str = round_bytes.decode('utf-8') if isinstance(round_bytes, bytes) else round_bytes
- return int(round_str)
- return 0
-
- except Exception as e:
- logger.error(f"❌ Redis读取轮次失败: {e}")
- return 0
-
- async def set_current_round(self, user_id: str, round_number: int) -> bool:
- """设置当前轮次"""
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_ROUND.format(user_id=user_id)
- await redis.setex(key, self.TTL_CONTEXT, str(round_number))
- return True
- except Exception as e:
- logger.error(f"❌ Redis设置轮次失败: {e}")
- return False
-
- async def increment_round(self, user_id: str) -> int:
- """轮次自增"""
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_ROUND.format(user_id=user_id)
- new_round = await redis.incr(key)
- await redis.expire(key, self.TTL_CONTEXT)
-
- # 同时更新 user:{user_id}:meta 中的 current_round(保持一致性)
- meta_key = f"user:{user_id}:meta"
- await redis.hset(meta_key, 'current_round', new_round)
- await redis.expire(meta_key, self.TTL_CONTEXT)
-
- logger.debug(f"✅ Redis轮次自增: user={user_id}, new_round={new_round}")
- return new_round
-
- except Exception as e:
- logger.error(f"❌ Redis轮次自增失败: {e}")
- return 0
-
- async def get_summary(self, user_id: str, end_round: int) -> Optional[str]:
- """获取历史总结"""
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_SUMMARY.format(user_id=user_id, end_round=end_round)
- summary_bytes = await redis.get(key)
-
- if summary_bytes:
- summary = summary_bytes.decode('utf-8') if isinstance(summary_bytes, bytes) else summary_bytes
- logger.info(f"⚡ Redis获取总结: user={user_id}, end_round={end_round}")
- return summary
-
- return None
-
- except Exception as e:
- logger.error(f"❌ Redis读取总结失败: {e}")
- return None
-
- async def set_summary(
- self,
- user_id: str,
- end_round: int,
- summary: str
- ) -> bool:
- """保存历史总结"""
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_SUMMARY.format(user_id=user_id, end_round=end_round)
- await redis.setex(key, self.TTL_SUMMARY, summary)
-
- logger.info(f"✅ Redis保存总结: user={user_id}, end_round={end_round}, 长度={len(summary)}字")
- return True
-
- except Exception as e:
- logger.error(f"❌ Redis保存总结失败: {e}")
- return False
-
- async def build_context_from_cache(
- self,
- user_id: str,
- current_round: int,
- db_manager=None
- ) -> Dict:
- """
- 从Redis构建完整上下文(超快)
-
- 逻辑:
- - 如果 current_round <= 15: 返回所有消息
- - 如果 current_round > 15: 返回所有历史总结 + 最近15轮
-
- Args:
- user_id: 用户ID
- current_round: 当前轮次
- db_manager: 数据库管理器(用于获取所有总结)
- """
- try:
- if current_round <= 15:
- # 前15轮:直接返回所有消息
- messages = await self.get_recent_messages(user_id, count=current_round * 2)
-
- return {
- "summary": None,
- "recent_messages": messages,
- "current_round": current_round,
- "total_tokens": self._estimate_tokens(messages=messages)
- }
- else:
- # 超过15轮:最新总结 + 最近15轮
- # 🔥 修复:查找 end_round < current_round 的最新总结
- # 因为总结是累积的,最新的总结包含了所有历史信息
- summary_end_round = current_round - 1
-
- logger.info(f"🔍 [Redis] 查询总结: user={user_id}, current_round={current_round}, summary_end_round={summary_end_round}")
-
- # 获取消息
- messages = await self.get_recent_messages(user_id, count=30) # 15轮=30条消息
-
- # 从数据库获取最新的一个总结(已包含所有历史信息,使用 ORM)
- summary = None
- if db_manager:
- try:
- from models_db import ConversationSummary
- from sqlalchemy import select, func
-
- async with db_manager.get_session() as session:
- # 先查询所有总结,用于调试
- all_summaries_result = await session.execute(
- select(
- ConversationSummary.end_round,
- ConversationSummary.start_round,
- ConversationSummary.created_at
- )
- .where(ConversationSummary.user_id == user_id)
- .order_by(ConversationSummary.end_round.desc())
- .limit(10)
- )
- all_summaries = [
- {
- 'end_round': row.end_round,
- 'start_round': row.start_round,
- 'created_at': row.created_at
- }
- for row in all_summaries_result.all()
- ]
-
- if all_summaries:
- logger.info(f"📊 [Redis] 数据库中的总结: user={user_id}, 总数={len(all_summaries)}, 列表={[(s['start_round'], s['end_round']) for s in all_summaries]}")
- else:
- logger.warning(f"⚠️ [Redis] 数据库中没有任何总结: user={user_id}")
-
- # 调试:查询数据库中所有不同的 user_id
- try:
- all_user_ids_result = await session.execute(
- select(
- ConversationSummary.user_id,
- func.count(ConversationSummary.id).label('summary_count')
- )
- .group_by(ConversationSummary.user_id)
- .limit(10)
- )
- all_user_ids = [
- {
- 'user_id': row.user_id,
- 'summary_count': row.summary_count
- }
- for row in all_user_ids_result.all()
- ]
- if all_user_ids:
- logger.info(f"🔍 [Redis] 数据库中存在的其他 user_id: {[(u['user_id'], u['summary_count']) for u in all_user_ids]}")
- except Exception as e:
- logger.debug(f"查询所有 user_id 失败: {e}")
-
- # 查询符合条件的总结
- summary_result = await session.execute(
- select(
- ConversationSummary.summary_text,
- ConversationSummary.end_round
- )
- .where(
- ConversationSummary.user_id == user_id,
- ConversationSummary.end_round <= summary_end_round
- )
- .order_by(ConversationSummary.end_round.desc())
- .limit(1)
- )
- summary_row = summary_result.first()
- if summary_row:
- summary = summary_row.summary_text
- end_round = summary_row.end_round
- logger.info(f"✅ [Redis] 获取最新总结: user={user_id}, end_round={end_round}, summary_length={len(summary)}")
- else:
- logger.warning(f"⚠️ [Redis] 未找到总结: user={user_id}, up_to_round={summary_end_round}, 数据库中总结数={len(all_summaries) if all_summaries else 0}")
- except Exception as e:
- logger.warning(f"⚠️ 从数据库获取总结失败: {e}")
- import traceback
- logger.error(traceback.format_exc())
-
- return {
- "summary": summary,
- "recent_messages": messages,
- "current_round": current_round,
- "total_tokens": self._estimate_tokens(summary=summary, messages=messages)
- }
-
- except Exception as e:
- logger.error(f"❌ 从Redis构建上下文失败: {e}")
- return {
- "summary": None,
- "recent_messages": [],
- "current_round": current_round,
- "total_tokens": 0
- }
-
- def _estimate_tokens(self, summary: str = None, messages: List[Dict] = None) -> int:
- """估算token数量"""
- total = 0
- if summary:
- total += len(summary) // 2
- if messages:
- for msg in messages:
- total += len(msg.get('content', '')) // 2
- return total
-
- async def invalidate_context(self, user_id: str) -> bool:
- """清除用户上下文缓存(强制刷新)"""
- try:
- redis = await self._get_redis()
- key = self.KEY_USER_CONTEXT.format(user_id=user_id)
- await redis.delete(key)
- logger.info(f"🗑️ Redis清除缓存: user={user_id}")
- return True
- except Exception as e:
- logger.error(f"❌ Redis清除失败: {e}")
- return False
-
- async def sync_from_db(self, user_id: str, messages: List[Dict], current_round: int) -> bool:
- """
- 从数据库同步数据到Redis(初始化缓存)
-
- Args:
- user_id: 用户ID
- messages: 消息列表
- current_round: 当前轮次
- """
- try:
- redis = await self._get_redis()
- # 清空旧数据
- messages_key = self.KEY_USER_MESSAGES.format(user_id=user_id)
- await redis.delete(messages_key)
-
- # 批量写入消息
- if messages:
- pipeline = redis.pipeline()
- for msg in messages:
- pipeline.rpush(
- messages_key,
- json.dumps(msg, ensure_ascii=False)
- )
- pipeline.expire(messages_key, self.TTL_MESSAGES)
- await pipeline.execute()
-
- # 设置轮次
- await self.set_current_round(user_id, current_round)
-
- logger.info(f"✅ Redis同步完成: user={user_id}, messages={len(messages)}, round={current_round}")
- return True
-
- except Exception as e:
- logger.error(f"❌ Redis同步失败: {e}")
- return False
-
- async def health_check(self) -> bool:
- """健康检查"""
- try:
- redis = await self._get_redis()
- await redis.ping()
- return True
- except Exception as e:
- logger.error(f"❌ Redis健康检查失败: {e}")
- return False
|