Какие есть варианты кэширования на уровне приложения?
Что проверяют
Знаете ли вы, какие уровни кэша доступны Python-сервису и как они ведут себя в реальном деплое: несколько воркеров Gunicorn, асинхронный код, общая база. Следом обычно спрашивают про инвалидацию или про то, почему lru_cache на методе класса течёт памятью.
Как отвечать
Идите от простого к распределённому:
- мемоизация функций:
functools.lru_cache(по умолчаниюmaxsize=128),functools.cacheбез ограничения размера с Python 3.9,functools.cached_propertyдля ленивых атрибутов. Аргументы должны быть хэшируемыми; - кэш с TTL в памяти:
cachetoolsсTTLCacheиLRUCache. Сам по себе он не потокобезопасен, при доступе из нескольких потоков нуженthreading.Lock; - на диске:
diskcache, когда данные крупные, а Redis заводить не хочется. Переживает перезапуск и доступен процессам на одной машине; - распределённый: Redis через
redis-py(для asyncio естьredis.asyncio) или Memcached. Общий для всех воркеров и реплик, поддерживает TTL и атомарные операции; - кэш фреймворка: в Django это cache framework с бэкендами, включая встроенный
RedisCacheс Django 4.0, декораторcache_pageдля представлений и кэш фрагментов шаблонов. Во Flask то же даёт Flask-Caching; - HTTP:
Cache-ControlиETagв ответах, кэш на nginx или CDN перед приложением.
Добавьте стратегию: cache-aside, версия схемы данных в ключе, TTL со случайным разбросом и явная инвалидация при записи.
Где спотыкаются
Забывают, что у каждого воркера Gunicorn или uWSGI свой процесс и свой lru_cache. Сброс кэша в одном воркере не виден остальным, а память расходуется столько раз, сколько воркеров.
Вешают lru_cache на метод. self становится частью ключа, и экземпляры не освобождаются сборщиком, пока живёт кэш.
Ставят lru_cache на async def. Кэшируется объект корутины, и повторный await падает с RuntimeError. Для асинхронного кода берут async-lru или пишут свою обёртку.