Files
chunyu_project/utils/async_cache.py
T

115 lines
3.5 KiB
Python

"""
异步 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