Files
XingHuo/app/service/risk/redis_gateway.py
T

107 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Redis 单例网关(B7 lifespan 管理连接):预警广播 publish + L3 缓存失效 DEL。
惰性连接(首次 publish/DEL 才建);连接/执行失败一律降级日志,不阻塞业务
(DB 为权威,PRD FR-4 通知语义)。测试注入 fake:monkeypatch 本模块
`_gateway`,实现 publish(channel, payload) / delete(*keys) 即可。
"""
from __future__ import annotations
import json
import logging
from typing import Any
from app.config.settings import settings
logger = logging.getLogger(__name__)
class RedisGateway:
def __init__(self, url: str | None = None) -> None:
self._url = url or settings.redis_url
self._client: Any = None
def _ensure(self) -> Any:
if self._client is None:
import redis
self._client = redis.Redis.from_url(self._url, decode_responses=True)
return self._client
def publish(self, channel: str, payload: dict[str, Any]) -> None:
self._ensure().publish(channel, json.dumps(payload, ensure_ascii=False))
def delete(self, *keys: str) -> None:
self._ensure().delete(*keys)
def exists(self, key: str) -> bool:
return bool(self._ensure().exists(key))
def set_ex(self, key: str, value: str, ttl_seconds: int) -> None:
self._ensure().setex(key, ttl_seconds, value)
# ---- 输入防护限流(T-03 · ratelimit:{agent}:{actor} 固定窗口)----
def incr(self, key: str) -> int:
"""原子自增并返回新值(INCR);调用方负责首命中时补 EXPIRE。"""
return int(self._ensure().incr(key))
# ---- 会话窗口(T-06 · sess:{agent}:{id}:msgs List)----
def rpush(self, key: str, *values: str) -> None:
self._ensure().rpush(key, *values)
def lrange(self, key: str, start: int, end: int) -> list[str]:
return self._ensure().lrange(key, start, end)
def ltrim(self, key: str, start: int, end: int) -> None:
self._ensure().ltrim(key, start, end)
def expire(self, key: str, ttl_seconds: int) -> None:
self._ensure().expire(key, ttl_seconds)
_gateway: RedisGateway | Any | None = None
def set_gateway(gateway: RedisGateway | Any | None) -> None:
"""单例注入点(B7 lifespan 注册;测试注入 fake)。"""
global _gateway
_gateway = gateway
def get_gateway() -> RedisGateway:
"""取网关单例;未注册时惰性创建(脚本直调场景)。"""
global _gateway
if _gateway is None:
_gateway = RedisGateway()
return _gateway
def publish(channel: str, payload: dict[str, Any]) -> None:
"""广播;失败降级(不阻塞预警落库)。"""
try:
get_gateway().publish(channel, payload)
except Exception:
logger.exception("publish %s failed", channel)
def cache_delete(*keys: str) -> None:
"""写侧缓存失效(PRD §5.1:MySQL 更新时 DEL);失败降级(TTL 兜底过期)。"""
try:
get_gateway().delete(*keys)
except Exception:
logger.warning("cache DEL failed (degrade to TTL): %s", keys, exc_info=True)
def key_exists(key: str) -> bool:
"""存在性检查(T-01 jti 吊销黑名单);失败 fail-open 返回 False。
日志不打 key 全文(jti 吊销 key 含完整 jti,手册 §11 只打前 8 位口径)。
"""
try:
return get_gateway().exists(key)
except Exception:
logger.warning("redis EXISTS failed (fail-open), key prefix=%s", key.split(":")[:2], exc_info=True)
return False