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.

PythonData Fetching

Code

snippet
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())