Точно скажу, что костыли и велосипеды не лучшее решение, особенно если мы говорим о кэшировании, а конкретнее, если нам надо оптимизировать метод доступа к данным, чтобы он имел производительность выше, чем на источнике. Я докажу это на нескольких примерах, приведённых в статье, всего за 5 минут.
Кэширование в теории и на практике
Прежде чем раскрыть всю суть, отмечу, что эта статья — продолжение нашего цикла про архитектуру highload-систем, где главным героем будет кэширование. Ранее, в материале «Big Data с «кремом» от LinkedIn: инструкция о том, как правильно строить архитектуру системы», я вскользь коснулся вопроса кэширования данных, как способа снижения нагрузки на СУБД, а значит, повышения производительности нашего приложения. Суть кэширования очень простая – не надо каждый запрос приземлять на СУБД. Как же это реализовать? Давайте разберёмся и начнём с определений и классификации.
Кэширование – это подход, который, при правильном (!) использовании значительно ускоряет работу и снижает нагрузку на вычислительные ресурсы. Если ещё проще, кэширование — это метод оптимизации хранения и/или доступа к данным, при котором операции с этими данными производятся эффективнее, чем на источнике.
Теперь о классификации — в рамках этой статьи я хочу подробнее остановиться на двух подходах: LRU-кэширование и кэширование в Redis.
Least Recently Used (Вытеснение давн�� неиспользуемых)
LRU — это алгоритм, при котором в первую очередь вытесняется неиспользованный дольше всех элемент.
Кэш, реализованный посредством стратегии LRU, упорядочивает элементы в порядке хронологии их использования. Каждый раз, когда мы обращаемся к записи, алгоритм LRU перемещает её в верхнюю часть кэша. Таким образом, алгоритм может быстро определить запись, которая дольше всех не использовалась, проверив конец списка.
В модуле стандартной библиотеки Python functools реализован декоратор @lru_cache, дающий возможность кэшировать результат выполнения функций, используя стратегию LRU.
Декоратор @lru_cache под капотом использует словарь. Результат выполнения функции кэшируется под ключом, который соответствует вызову функции и её аргументам. О чём это говорит? Самые догадливые уже сообразили: чтобы декоратор работал, — аргументы должны быть хешируемыми.
Вот пример:
Нам нужно пробежаться по журналу событий (audit_log), где каждый элемент имеет атрибут user_id — уникальный идентификатор пользователя, подтверждающий определённое действие пользователем в информационной системе. При этом, один и тот же пользователь обычно совершает множественные действия в системе, а значит, событий с одинаковыми used_id будет больше 1. Но идентификатор пользователя нам ни о чём не говорит. Это просто UUID и если вы не вундеркинд, который запоминает 100 знаков после запятой в числе π, то вам проще оперировать фамилией, именем и отчеством (ФИО). А где лежит ФИО? Правильно — в СУБД, в табличке с пользователями. И что теперь каждый раз делать запрос в СУБД по одному и тому же user_id, чтобы получить ФИО? Конечно, нет!
Применим LRU декоратор уже на конкретном примере:
from functools import lru_cache from pymongo import MongoClient # открываем соединение к MongoDb # получаем доступ к нашей коллекции с пользователями client = MongoClient("localhost:27017") collection = client.users_info # функция для получения ФИО по id @lru_cache def get_fio_by_id(id) doc = collection.fing_one({"_id": id}) if not doc: return None return doc["fio"] # итерируемся по журналу событий for event in audit_log: # для конкретного события получаем идентификатор user_id user_id = event.get("user_id") if user_id: # резолвим id в ФИО print(get_fio_by_id(user_id))
Одна строчка кода, которая декорирует функцию get_fio_by_id() и мы уже прикрутили LRU кэш + существенно повысили производительность приложения!
А точно не нужны велосипеды?
А что, если пойти ещё дальше? Мы же имеем большое распределённое приложение, и многие микросервисы ходят в СУБД за одинаковыми справочными данными. Конечно, можно везде накрутить LRU-кэши, но при этом, нам всё равно придётся из каждого сервиса делать запросы в СУБД за одинаковыми данными. Чтобы этого избежать, давайте использовать централизованный отказоустойчивый кэш на базе Redis!

Возможно ли обойтись одной строчкой в коде как в случае с LRU? Да! Самые внимательные и опытные уже наверняка догадались – нам нужен «Декоратор!».
Easy, here we go:
import json from functools import wraps from redis import StrictRedis redis = StrictRedis() def redis_cache(func): @wraps(func) def wrapper(*args, **kwargs): # собираем ключ из аргументов ф-и. key_parts = [func.__name__] + list(args) key = '-'.join(key_parts) result = redis.get(key) if result is None: # ничего не нашли в кэше – дергаем ф-ю и сохраняем результат. value = func(*args, **kwargs) value_json = json.dumps(value) redis.set(key, value_json) else: # Ура, данные есть в кэше – используем их. value_json = result.decode('utf-8') value = json.loads(value_json) return value return wrapper
Если тут совсем ничего не понятно, то пора повторять матчасть по устройству декораторов в python.
Остался последний шаг — вишенка на торте в нашем декоре. Чтобы всё это гениальное творение использовать в приложении — просто меняем декоратор @lru_cache на @redis_cache.
UPDATE: Как справедливо отметили внимательные читатели, существует множество готовых к использованию библиотек для различных способов и стратегий кеширования: aiocache, cacheout, python-cache, cachetools. Список бесконечный...
That’s it!
