This commit is contained in:
2026-09-09 11:35:02 +08:00
parent bc1c72d558
commit d656c05b3d
35 changed files with 711 additions and 3194 deletions

View File

@@ -1,7 +1,8 @@
"""Redis 读缓存(可选基础设施)。
- REDIS_URL 留空、连接失败或超时:所有操作静默退化为「无缓存」,接口照常直查数据库
且本进程内禁用重试(避免每个请求都陪跑一次连接超时)。
- REDIS_URL 留空、连接失败或超时:所有操作静默退化为「无缓存」,接口照常直查数据库
故障后熔断 _RECOVERY_SECONDS期间所有请求直连不陪跑连接超时到期自动放行
一次探测——成功即完全恢复仍失败则重新熔断Redis 属加速件,坏了不能拖慢接口)。
- 失效策略TTL 自然过期 + 版本号INCR作废。自选股增删等写操作只 INCR 版本 key
旧缓存 key 里带着旧版本号,无需 SCAN 批量删除。
- 只缓存「读多写少、可容忍短暂陈旧」的聚合数据(股票列表、筛选项等);
@@ -13,6 +14,7 @@
"""
from __future__ import annotations
import asyncio
import hashlib
import json
import time
@@ -23,12 +25,13 @@ import redis.asyncio as aioredis
from .config import settings
_pool: aioredis.ConnectionPool | None = None
_disabled = False # 一次失败后本进程禁用Redis 属加速件,坏了不能拖慢接口)
_RECOVERY_SECONDS = 60.0 # 熔断时长:期间直连不试 Redis到期放行一次探测
_disabled_until = 0.0 # 熔断截止的 monotonic 时刻0 = 未熔断
def _client() -> aioredis.Redis | None:
global _pool, _disabled
if not settings.redis_url or _disabled:
global _pool
if not settings.redis_url or time.monotonic() < _disabled_until:
return None
if _pool is None:
_pool = aioredis.ConnectionPool.from_url(
@@ -43,8 +46,8 @@ def _client() -> aioredis.Redis | None:
def _bail() -> None:
global _disabled
_disabled = True
global _disabled_until
_disabled_until = time.monotonic() + _RECOVERY_SECONDS
def digest(*parts: Any) -> str:
@@ -84,7 +87,21 @@ def local_set(key: str, raw: str, ttl: int) -> None:
_local_store[key] = (time.monotonic() + max(1, min(ttl, 120)), raw)
_local_bytes += len(raw) + 64 # 连同 dict/tuple 开销粗略计入
while _local_store and (len(_local_store) > _LOCAL_MAX_ENTRIES or _local_bytes > _LOCAL_MAX_BYTES):
_local_bytes -= len(_local_store.popitem(last=False)[1][1]) + 64
# dict 是插入序next(iter(...)) 即最旧键dict.popitem 不支持参数,别写成 OrderedDict 的写法)
oldest = next(iter(_local_store))
_local_bytes -= len(_local_store.pop(oldest)[1]) + 64
# --- 后台写fire-and-forget------------------------------------------------
# 读路径拿到响应后异步写 Redis、不阻塞返回。统一入口任务挂全局集合防 GC
# cache_set 内部自带异常静默缓存层尽力而为优雅停机丢最后一次写无害TTL 兜底)。
_bg_tasks: set[asyncio.Task] = set()
def set_bg(key: str, value: Any, ttl: int) -> None:
t = asyncio.create_task(cache_set(key, value, ttl))
_bg_tasks.add(t)
t.add_done_callback(_bg_tasks.discard)
# --- 版本号本地缓存:热请求连 Redis GET ver:xx 都省掉 ------------------------