Не изобретая велосипед. Кэширование: рассказываем главные секреты оптимизации доступа к данным

от автора

Точно скажу, что костыли и велосипеды не лучшее решение, особенно если мы говорим о кэшировании, а конкретнее, если нам надо оптимизировать метод доступа к данным, чтобы он имел производительность выше, чем на источнике. Я докажу это на нескольких примерах, приведённых в статье, всего за 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 from json import loads  # открываем соединение к 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.

That’s it!


ссылка на оригинал статьи https://habr.com/ru/company/stm_labs/blog/654201/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *