← 中文资源

简体中文版本

Redis 异步缓存装饰器

用装饰器把异步函数结果缓存进 Redis:按调用签名取键、TTL 过期、NX 锁单飞防止并发回源风暴,JSON 序列化透明完成。

阅读英文原文

示例说明

缓存装饰器让业务函数零改动获得缓存能力:调用方照常 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())