124 lines
3.7 KiB
Python
124 lines
3.7 KiB
Python
|
|
"""
|
|||
|
|
简单高效的缓存层
|
|||
|
|
- 内存 LRU 缓存,每个 worker 独立
|
|||
|
|
- 短 TTL 避免内容过时
|
|||
|
|
- 针对不同路径设置不同 TTL
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import hashlib
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
from collections import OrderedDict
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
|
|||
|
|
class LRUCache:
|
|||
|
|
"""Thread-safe LRU cache with TTL support."""
|
|||
|
|
|
|||
|
|
def __init__(self, capacity: int = 2000, default_ttl: int = 30):
|
|||
|
|
self.capacity = capacity
|
|||
|
|
self.default_ttl = default_ttl
|
|||
|
|
self._cache: OrderedDict[str, tuple[float, bytes, dict, int]] = OrderedDict()
|
|||
|
|
# (expiry_time, body, headers, status)
|
|||
|
|
self._lock = threading.RLock()
|
|||
|
|
|
|||
|
|
def _make_key(self, url: str, headers: Optional[dict] = None) -> str:
|
|||
|
|
"""Generate cache key from URL and key headers."""
|
|||
|
|
# Use URL + relevant headers
|
|||
|
|
accept = ""
|
|||
|
|
if headers:
|
|||
|
|
accept = headers.get("Accept-Encoding", "")
|
|||
|
|
raw = f"{url}|{accept}"
|
|||
|
|
return hashlib.md5(raw.encode()).hexdigest()
|
|||
|
|
|
|||
|
|
def get(self, url: str, headers: Optional[dict] = None) -> Optional[tuple[bytes, dict, int]]:
|
|||
|
|
"""Get cached response. Returns (body, headers, status) or None."""
|
|||
|
|
key = self._make_key(url, headers)
|
|||
|
|
with self._lock:
|
|||
|
|
if key not in self._cache:
|
|||
|
|
return None
|
|||
|
|
expiry, body, resp_headers, status = self._cache[key]
|
|||
|
|
if time.time() > expiry:
|
|||
|
|
del self._cache[key]
|
|||
|
|
return None
|
|||
|
|
# Move to end (most recently used)
|
|||
|
|
self._cache.move_to_end(key)
|
|||
|
|
return (body, resp_headers, status)
|
|||
|
|
|
|||
|
|
def set(self, url: str, body: bytes, headers: dict, status: int,
|
|||
|
|
ttl: Optional[int] = None, request_headers: Optional[dict] = None):
|
|||
|
|
"""Store response in cache."""
|
|||
|
|
key = self._make_key(url, request_headers)
|
|||
|
|
t = ttl if ttl is not None else self.default_ttl
|
|||
|
|
expiry = time.time() + t
|
|||
|
|
|
|||
|
|
with self._lock:
|
|||
|
|
self._cache[key] = (expiry, body, headers, status)
|
|||
|
|
self._cache.move_to_end(key)
|
|||
|
|
if len(self._cache) > self.capacity:
|
|||
|
|
self._cache.popitem(last=False)
|
|||
|
|
|
|||
|
|
def invalidate(self, url: str, headers: Optional[dict] = None):
|
|||
|
|
"""Remove a specific URL from cache."""
|
|||
|
|
key = self._make_key(url, headers)
|
|||
|
|
with self._lock:
|
|||
|
|
self._cache.pop(key, None)
|
|||
|
|
|
|||
|
|
def clear(self):
|
|||
|
|
with self._lock:
|
|||
|
|
self._cache.clear()
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def size(self) -> int:
|
|||
|
|
with self._lock:
|
|||
|
|
return len(self._cache)
|
|||
|
|
|
|||
|
|
def get_stats(self) -> dict:
|
|||
|
|
with self._lock:
|
|||
|
|
return {
|
|||
|
|
"size": len(self._cache),
|
|||
|
|
"capacity": self.capacity,
|
|||
|
|
"usage_pct": round(len(self._cache) / self.capacity * 100, 1) if self.capacity else 0,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
# TTL 策略:不同路径不同缓存时间
|
|||
|
|
PATH_TTL = {
|
|||
|
|
"/": 30, # 首页 30s
|
|||
|
|
"/works": 60, # 作品列表 60s
|
|||
|
|
"/chapters": 120, # 章节内容 120s
|
|||
|
|
"/series": 60, # 系列 60s
|
|||
|
|
"/collections": 60,
|
|||
|
|
"/tags": 60,
|
|||
|
|
"/users": 30,
|
|||
|
|
"/pseuds": 30,
|
|||
|
|
"/bookmarks": 60,
|
|||
|
|
"/skins": 300, # CSS 皮肤缓存 5 分钟
|
|||
|
|
"/stylesheets": 300,
|
|||
|
|
"/images": 600, # 图片缓存 10 分钟
|
|||
|
|
"/media": 600,
|
|||
|
|
"/javascripts": 300,
|
|||
|
|
"/api": 15, # API 响应 15s
|
|||
|
|
"/external_links": 30,
|
|||
|
|
# Default: 30s
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_ttl_for_path(path: str) -> int:
|
|||
|
|
"""Determine cache TTL based on URL path."""
|
|||
|
|
for prefix, ttl in PATH_TTL.items():
|
|||
|
|
if path.startswith(prefix):
|
|||
|
|
return ttl
|
|||
|
|
return 30 # default
|
|||
|
|
|
|||
|
|
|
|||
|
|
# 全局缓存实例
|
|||
|
|
_cache: Optional[LRUCache] = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_cache() -> LRUCache:
|
|||
|
|
global _cache
|
|||
|
|
if _cache is None:
|
|||
|
|
_cache = LRUCache(capacity=5000, default_ttl=30)
|
|||
|
|
return _cache
|