示例说明
缓存装饰器让业务函数零改动获得缓存能力:调用方照常 await,装饰器先查 Redis 命中即返回。键由模块名、函数名与参数签名拼成,不同调用互不串扰;缓存失效瞬间的并发请求用 NX 锁做单飞——只有一个请求真正回源,其余稍候后读刚写入的结果,下游免遭击穿。
技术要点
- 调用签名构成唯一缓存键
- NX 锁实现回源单飞
- 锁的 finally 释放保证异常路径清洁
代码解读
wrapper 先 GET 命中直接反序列化返回;未命中尝试拿锁,拿到者执行原函数并写回带 TTL 的结果后删锁;没拿到锁的并发请求小睡后重读缓存,兜底再回源。gather 并发两次调用演示单飞效果——回源只发生一次,两个结果一致。
使用建议
不可 JSON 序列化的返回值需要自定义编码器。写操作后要删除对应键显式失效。锁等待的睡眠值按回源耗时调优,失败兜底要有限次。
代码示例
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())