modify: JWT 结果加进程内一级缓存(L1),免每请求共享缓存同步往返(+45~68% 吞吐)

This commit is contained in:
2026-10-13 01:59:05 +08:00
parent c92b34a55f
commit c87b7baa64
+61 -5
View File
@@ -1,5 +1,7 @@
import hashlib import hashlib
import os
import time import time
from collections import OrderedDict
from fastapi import Depends, HTTPException, status, Request from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -21,6 +23,45 @@ _jwt_cache = Cache.from_config(get_config()["cache"])
# 安全性由命中时的 exp 复核保证,TTL 不承担过期判断 # 安全性由命中时的 exp 复核保证,TTL 不承担过期判断
_JWT_CACHE_TTL = 60 _JWT_CACHE_TTL = 60
# ── 一级:进程内 JWT 结果缓存(LRU + TTL)────────────────────────────
# 鉴权依赖是同步依赖(def),每次请求都要走一遍共享缓存——backend=redis 时
# 即"每请求一次同步网络往返 + FastAPI 线程池 hop"。同一 token 在 TTL 内被
# 反复使用是常态,故在进程内加一层 L1:命中直接返回,不碰网络。
# 语义与 L2 完全一致:key 含密钥/算法指纹;命中仍复核 exp(过期=拒绝而非放行);
# 条目按 TTL 惰性清理并限长。CHONGMING_JWT_L1=0 可关闭(便于 A/B 与排障)。
_JWT_L1_ENABLED = os.environ.get("CHONGMING_JWT_L1", "1") != "0"
_JWT_L1_MAX = 1024
# 存 orjson 字节而非 dict:命中时反序列化出新副本,保证"调用方修改返回值
# 不污染缓存"这一 L2 既有契约(L2 同样是 JSON 往返后返回新对象)
_jwt_l1: "OrderedDict[str, tuple[bytes, float, float]]" = OrderedDict() # key → (payload_json, exp, 写入时刻)
def _l1_get(key: str):
"""命中返回 (payload_json: bytes, exp);条目超过共享缓存 TTL(运行时可调,便于测试/运维)视为过期"""
entry = _jwt_l1.get(key)
if entry is None:
return None
_payload_json, _exp, ts = entry
if time.time() - ts >= _JWT_CACHE_TTL:
_jwt_l1.pop(key, None)
return None
_jwt_l1.move_to_end(key)
return entry
def _l1_set(key: str, payload_json: bytes, exp: float) -> None:
_jwt_l1[key] = (payload_json, exp, time.time())
_jwt_l1.move_to_end(key)
while len(_jwt_l1) > _JWT_L1_MAX:
_jwt_l1.popitem(last=False)
def _l1_sweep(now: float) -> None:
"""惰性清理:超 TTL 的整条丢弃(token 永不重复时防堆积),只在表较大时调用"""
stale = [k for k, v in _jwt_l1.items() if now - v[2] >= _JWT_CACHE_TTL]
for k in stale:
_jwt_l1.pop(k, None)
def _cache_key(token: str, secret: str, algorithm: str) -> str: def _cache_key(token: str, secret: str, algorithm: str) -> str:
"""缓存 key:token 内容 + 密钥/算法指纹,不同密钥/算法不串用""" """缓存 key:token 内容 + 密钥/算法指纹,不同密钥/算法不串用"""
@@ -28,20 +69,33 @@ def _cache_key(token: str, secret: str, algorithm: str) -> str:
def _cached_decode(token: str, secret: str, algorithm: str) -> tuple: def _cached_decode(token: str, secret: str, algorithm: str) -> tuple:
"""JWT 解密结果缓存((payload, exp)),共享缓存避免高频请求重复密码学运算。 """JWT 解密结果缓存((payload, exp)):一级进程内 LRU,二级共享缓存。
- 命中且未过期 -> 直接返回,跳过 RSA/ECDSA 验证与 JSON 解析 - 一级命中且未过期 -> 直接返回(零网络、零密码学);过期 -> 直接拒绝
- 命中但 exp 已过 -> 直接拒绝,不重解密(条目保留至 TTL 到期, - 一级未命中 -> 查二级共享缓存;命中且未过期 -> 回填一级后返回
过期 token 的重复请求零密码学开销;缓存不延长 token 有效期) - 二级也未命中 -> 完整解码(RSA/ECDSA 验证)后写二级 + 一级(TTL=60s/30s)
- 未命中 -> 完整解码后写入共享缓存(TTL=60s)
- decode 失败(无效 token)不缓存,每次都完整验证后拒绝 - decode 失败(无效 token)不缓存,每次都完整验证后拒绝
""" """
key = _cache_key(token, secret, algorithm) key = _cache_key(token, secret, algorithm)
now = time.time()
if _JWT_L1_ENABLED:
hit = _l1_get(key)
if hit is not None:
payload_json, exp, _ts = hit
if not exp or exp >= now:
# 反序列化出新副本:调用方修改不影响缓存条目(与 L2 契约一致)
return orjson.loads(payload_json), exp
raise ValueError("Token 已过期")
if len(_jwt_l1) > _JWT_L1_MAX // 2: # 仅大表时惰性清理,避免常态 O(n)
_l1_sweep(now)
entry = _jwt_cache.get(key) entry = _jwt_cache.get(key)
if entry is not None: if entry is not None:
payload = entry["payload"] payload = entry["payload"]
exp = entry.get("exp", 0) exp = entry.get("exp", 0)
if not exp or exp >= time.time(): if not exp or exp >= time.time():
if _JWT_L1_ENABLED:
_l1_set(key, orjson.dumps(payload), exp)
return payload, exp return payload, exp
raise ValueError("Token 已过期") raise ValueError("Token 已过期")
jwt = JWT(secret, algorithm) jwt = JWT(secret, algorithm)
@@ -49,6 +103,8 @@ def _cached_decode(token: str, secret: str, algorithm: str) -> tuple:
payload = orjson.loads(payload_str.encode("utf-8")) payload = orjson.loads(payload_str.encode("utf-8"))
exp = payload.get("exp", 0) exp = payload.get("exp", 0)
_jwt_cache.set(key, {"payload": payload, "exp": exp}, expire=_JWT_CACHE_TTL) _jwt_cache.set(key, {"payload": payload, "exp": exp}, expire=_JWT_CACHE_TTL)
if _JWT_L1_ENABLED:
_l1_set(key, orjson.dumps(payload), exp)
return payload, exp return payload, exp