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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
缓存服务
基于 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