feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
异步 Redis 缓存封装 —— 100% 异步化基建
|
||||
========================================
|
||||
Django 的 cache 框架(django-redis)没有异步 API,本模块基于 redis.asyncio 直连,
|
||||
复用 settings 中的 REDIS 配置,提供与 django cache 对等的异步接口。
|
||||
|
||||
用法:
|
||||
from utils.async_cache import aget_cache, aset_cache, adelete_cache
|
||||
|
||||
value = await aget_cache('key')
|
||||
await aset_cache('key', obj, timeout=3600)
|
||||
await adelete_cache('key')
|
||||
|
||||
序列化:msgpack(快速紧凑)+ JSON 兜底;与 django-redis 缓存池互不干扰(独立 key 前缀)。
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from django.conf import settings
|
||||
from django.core.cache import caches
|
||||
from asgiref.sync import sync_to_async
|
||||
import msgpack
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_cache_pool = None
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
|
||||
async def _get_pool():
|
||||
"""懒初始化 redis.asyncio 连接池(复用 settings.REDIS 配置)"""
|
||||
global _cache_pool
|
||||
async with _lock:
|
||||
if _cache_pool is None:
|
||||
import redis.asyncio as aioredis
|
||||
_cache_pool = aioredis.Redis(
|
||||
host=settings.REDIS_HOST,
|
||||
port=int(settings.REDIS_PORT),
|
||||
db=int(getattr(settings, 'REDIS_DB', 0)),
|
||||
password=getattr(settings, 'REDIS_PASSWORD', '') or None,
|
||||
decode_responses=False,
|
||||
socket_connect_timeout=5,
|
||||
socket_timeout=5,
|
||||
max_connections=50,
|
||||
)
|
||||
return _cache_pool
|
||||
|
||||
|
||||
def _pack(value):
|
||||
try:
|
||||
return b'async-cache:' + msgpack.packb(value, use_bin_type=True, default=str)
|
||||
except (TypeError, ValueError):
|
||||
return b'async-cache-json:' + json.dumps(value, ensure_ascii=False, default=str).encode()
|
||||
|
||||
|
||||
def _unpack(raw):
|
||||
if raw is None:
|
||||
return None
|
||||
if raw.startswith(b'async-cache-json:'):
|
||||
return json.loads(raw[17:].decode())
|
||||
return msgpack.unpackb(raw[12:], raw=False)
|
||||
|
||||
|
||||
async def aget_cache(key, default=None):
|
||||
try:
|
||||
pool = await _get_pool()
|
||||
raw = await pool.get(f'async:{key}')
|
||||
if raw is None:
|
||||
return default
|
||||
return _unpack(raw)
|
||||
except Exception:
|
||||
logger.warning('aget_cache failed for %s, falling back to sync cache', key)
|
||||
try:
|
||||
return await sync_to_async(caches['default'].get)(key, default)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
async def aset_cache(key, value, timeout=300):
|
||||
try:
|
||||
pool = await _get_pool()
|
||||
await pool.set(f'async:{key}', _pack(value), ex=timeout)
|
||||
return True
|
||||
except Exception:
|
||||
logger.warning('aset_cache failed for %s, falling back to sync cache', key)
|
||||
try:
|
||||
await sync_to_async(caches['default'].set)(key, value, timeout)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def adelete_cache(key):
|
||||
try:
|
||||
pool = await _get_pool()
|
||||
await pool.delete(f'async:{key}')
|
||||
return True
|
||||
except Exception:
|
||||
logger.warning('adelete_cache failed for %s', key)
|
||||
try:
|
||||
await sync_to_async(caches['default'].delete)(key)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
async def aget_or_set(key, factory, timeout=300):
|
||||
"""原子性不保证(与 django cache 用法一致),value 为 await factory() 结果"""
|
||||
v = await aget_cache(key)
|
||||
if v is not None:
|
||||
return v
|
||||
v = await factory() if asyncio.iscoroutinefunction(factory) else factory()
|
||||
await aset_cache(key, v, timeout)
|
||||
return v
|
||||
Reference in New Issue
Block a user