async-lru
async-lru
async-lru — это библиотека для кэширования результатов асинхронных функций. По смыслу она похожа на стандартный functools.lru_cache, но рассчитана на asyncio: она умеет кешировать awaitable-результаты и не допускает гонок при одновременных вызовах одного и того же ключа.
Главная особенность: если несколько задач одновременно обращаются к одной и той же функции с одинаковыми аргументами, то реально будет выполнен только один вызов. Все остальные await получат тот же результат после завершения вычисления.
Это особенно полезно для:
- HTTP-запросов к внешним API;
- чтения данных из Redis/DB/Postgres;
- повторных вычислений на одних и тех же аргументах;
- уменьшения thundering herd при массовом параллельном доступе.
Базовый пример
import asyncio
from async_lru import alru_cache
@alru_cache(maxsize=128)
async def fetch_user(user_id: int):
await asyncio.sleep(0.5)
return {"id": user_id, "name": f"user-{user_id}"}
async def main():
first = await fetch_user(42)
second = await fetch_user(42)
print(first)
print(second)
print(fetch_user.cache_info())
await fetch_user.cache_close()
asyncio.run(main())
Что происходит здесь:
@alru_cache(maxsize=128)добавляет LRU-кэш для async-функции;- одинаковые аргументы возвращают данные из кэша;
- разовое вычисление результата делится между всеми конкурентными вызовами;
cache_info()показывает статистику: hits, misses, currsize, maxsize.
Почему это важно
Обычный кэш для async-функций часто пишут руками через словарь и asyncio.Lock, но это быстро превращается в ошибочный и сложный код. async-lru решает типичную проблему: “несколько задач одновременно могут начать одно и то же expensive-вычисление”.
Без защиты такой сценарий приводит к повторным запросам к API, лишним вычислениям и лавине параллельных вызовов. async-lru как раз устраняет этот эффект, сохраняя семантику LRU-кэша.
TTL и expiration
Библиотека поддерживает TTL (time-to-live) для кэшированных значений. Это удобно, если данные быстро устаревают и хочется периодически обновлять их без полного сброса кэша.
from async_lru import alru_cache
@alru_cache(ttl=5)
async def get_config(name: str):
return name.upper()
Для более мягкого обновления можно использовать jitter:
@alru_cache(ttl=3600, jitter=1800)
async def fetch_data(key):
...
Тогда TTL каждого элемента будет случайно “размазаны” по диапазону, что помогает избегать массовой синхронной просушки кэша.
Инвалидация и проверка содержимого
Библиотека умеет явно очищать элементы кэша.
@alru_cache(ttl=5)
async def func(arg1, arg2):
return arg1 + arg2
func.cache_invalidate(1, arg2=2)
Также есть проверка, есть ли конкретный ключ в кэше:
await func(1, arg2=2)
print(func.cache_contains(1, arg2=2)) # True
И методы для очистки всего кэша:
func.cache_clear()
Кастомные ключи
По умолчанию ключ строится из аргументов функции так же, как functools.lru_cache. Но иногда часть аргументов не должна участвовать в ключе. Для этого есть параметр key.
from async_lru import alru_cache
@alru_cache(key=lambda db, query: query)
async def query_db(db, query):
return await db.execute(query)
Это удобно, когда один и тот же запрос должен кэшироваться независимо от того, какой именно объект соединения использовался.
Ограничения и важные детали
У async-lru есть важное ограничение: кэш привязан к конкретному event loop. Если один и тот же декорированный объект начать использовать в другом event loop, библиотека выбросит RuntimeError.
Это нормально для типичных asyncio-приложений, где один loop на приложение, но важно помнить:
- если сервис работает на нескольких loop, нужно создавать отдельные кэш-экземпляры;
- важно не смешивать одни и те же cached-функции между потоками или loop.
Также библиотека строит ключ только из явных аргументов. Если результат зависит от contextvars, request-scoped state, текущего пользователя, tenant id или глобальных переменных, то это нужно явно включать в аргументы функции или использовать отдельный кэш для каждого security-domain.
Когда использовать async-lru
Используйте async-lru, когда:
- у вас async-функция делает expensive I/O или вычисления;
- один и тот же запрос может повторяться много раз;
- важно не только кэшировать результаты, но и дедуплицировать concurrent calls;
- нужен простой LRU-подход без написания кастомного кэша.
Типичные сценарии:
- кеширование response body из API;
- мемоизация запросов к базе и сервисам;
- уменьшение повторной нагрузки при burst-traffic.
Сравнение с functools.lru_cache
functools.lru_cache хорошо работает для sync-функций, но не подходит для async-кода. async-lru же:
- работает с
async def; - обеспечивают совместный результат для concurrent requests;
- поддерживает TTL и явную инвалидацию;
- сохраняет идеи LRU и
cache_infoв привычной форме.
Дополнительно
Источник
AI-warning
✨ Данная статья сгенерирована с помощью ChatGPT 5.0 mini. Текст проверен человеком.