You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
5.9 KiB

"""
缓存服务
基于 Redis 实现 K 线数据缓存
"""
import json
import logging
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Any
import redis.asyncio as redis
from app.config import settings
logger = logging.getLogger(__name__)
class CacheService:
"""
缓存服务
提供 K 线数据的 Redis 缓存功能
- 缓存键格式kline:{symbol}:{period}:{start}:{end}
- 默认 TTL300 5 分钟
"""
def __init__(self):
self.redis: Optional[redis.Redis] = None
self._connected = False
self.default_ttl = 300 # 5 分钟
async def connect(self) -> bool:
"""连接 Redis"""
try:
self.redis = redis.from_url(settings.REDIS_URL, decode_responses=True)
await self.redis.ping()
self._connected = True
logger.info("Redis cache connected")
return True
except Exception as e:
logger.error(f"Redis connection failed: {e}")
self._connected = False
return False
async def disconnect(self):
"""断开 Redis 连接"""
if self.redis:
await self.redis.close()
self._connected = False
logger.info("Redis cache disconnected")
def _make_key(
self,
symbol: str,
period: str,
start: str,
end: str
) -> str:
"""生成缓存键"""
return f"kline:{symbol}:{period}:{start}:{end}"
async def get_kline(
self,
symbol: str,
period: str,
start: str,
end: str
) -> Optional[List[Dict]]:
"""
从缓存获取 K 线数据
Args:
symbol: 证券代码
period: 周期
start: 开始时间
end: 结束时间
Returns:
K 线数据列表缓存未命中返回 None
"""
if not self._connected:
return None
try:
key = self._make_key(symbol, period, start, end)
data = await self.redis.get(key)
if data:
logger.debug(f"Cache hit: {key}")
return json.loads(data)
logger.debug(f"Cache miss: {key}")
return None
except Exception as e:
logger.error(f"Cache get error: {e}")
return None
async def set_kline(
self,
symbol: str,
period: str,
start: str,
end: str,
data: List[Dict],
ttl: Optional[int] = None
) -> bool:
"""
写入 K 线数据到缓存
Args:
symbol: 证券代码
period: 周期
start: 开始时间
end: 结束时间
data: K 线数据
ttl: 过期时间默认使用 default_ttl
Returns:
是否成功
"""
if not self._connected:
return False
try:
key = self._make_key(symbol, period, start, end)
serialized = json.dumps(data, ensure_ascii=False)
ttl = ttl or self.default_ttl
await self.redis.setex(key, ttl, serialized)
logger.debug(f"Cache set: {key} (TTL={ttl}s)")
return True
except Exception as e:
logger.error(f"Cache set error: {e}")
return False
async def delete_kline(
self,
symbol: str,
period: str,
start: str,
end: str
) -> bool:
"""删除缓存"""
if not self._connected:
return False
try:
key = self._make_key(symbol, period, start, end)
await self.redis.delete(key)
logger.debug(f"Cache deleted: {key}")
return True
except Exception as e:
logger.error(f"Cache delete error: {e}")
return False
async def clear_kline_cache(self, symbol: Optional[str] = None) -> int:
"""
清除 K 线缓存
Args:
symbol: 如果指定只清除该品种的缓存
Returns:
清除的键数量
"""
if not self._connected:
return 0
try:
if symbol:
pattern = f"kline:{symbol}:*"
else:
pattern = "kline:*"
count = 0
async for key in self.redis.scan_iter(match=pattern):
await self.redis.delete(key)
count += 1
logger.info(f"Cleared {count} cache keys (pattern={pattern})")
return count
except Exception as e:
logger.error(f"Cache clear error: {e}")
return 0
async def get_stats(self) -> Dict[str, Any]:
"""获取缓存统计信息"""
if not self._connected:
return {"connected": False}
try:
info = await self.redis.info("stats")
keys_count = await self.redis.dbsize()
# 统计 kline 相关键
kline_keys_count = 0
async for _ in self.redis.scan_iter(match="kline:*"):
kline_keys_count += 1
return {
"connected": True,
"total_keys": keys_count,
"kline_cache_keys": kline_keys_count,
"hits": info.get("keyspace_hits", 0),
"misses": info.get("keyspace_misses", 0)
}
except Exception as e:
logger.error(f"Cache stats error: {e}")
return {"connected": False, "error": str(e)}
# 全局缓存服务实例
cache_service = CacheService()
async def get_cache_service() -> CacheService:
"""获取缓存服务实例"""
return cache_service