Redis Py Async Cache Decorator
Cache async function results in Redis with a decorator, TTL expiry, single-flight locking, and JSON serialization per call signature.
Code
import asyncio
import functools
import json
import redis.asyncio as redis
pool = redis.ConnectionPool.from_url('redis://localhost:6379/0')
client = redis.Redis(connection_pool=pool)
def async_cache(ttl: int = 60):
def decorator(fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
key = f'cache:{fn.__module__}:{fn.__qualname__}:{args!r}:{sorted(kwargs.items())!r}'
cached = await client.get(key)
if cached is not None:
return json.loads(cached)
lock_key = key + ':lock'
# 简化版单飞:拿不到锁的并发请求稍候后直接读缓存
got_lock = await client.set(lock_key, '1', nx=True, ex=ttl)
if got_lock:
try:
value = await fn(*args, **kwargs)
await client.set(key, json.dumps(value), ex=ttl)
return value
finally:
await client.delete(lock_key)
await asyncio.sleep(0.05)
cached = await client.get(key)
if cached is not None:
return json.loads(cached)
return await fn(*args, **kwargs)
return wrapper
return decorator
@async_cache(ttl=90)
async def exchange_rates(base: str) -> dict:
print(f'cache miss: fetching rates for {base}')
await asyncio.sleep(0.3)
return {'base': base, 'usd': 1.0, 'eur': 0.92}
async def main() -> None:
first, second = await asyncio.gather(
exchange_rates('usd'), exchange_rates('usd')
)
print('single flight result equal:', first == second)
await client.aclose()
asyncio.run(main())Report a problem with this exampleOpens a pre-filled issue on GitHub.