*Эта статья изначально была написана Джонатаном Майлзом в Блоге разработчиков Honeybadger.

Кэширование — это общий термин, который означает сохранение результата некоторого кода, чтобы мы могли быстро получить его позже. Это позволяет нам, например, один раз выполнить тяжелую обработку чисел, а затем просто повторно использовать значение без необходимости его повторного пересчета. Хотя общая концепция одинакова для всех типов кэширования, существуют различные механизмы, которые мы можем использовать в зависимости от того, что мы пытаемся кэшировать.

Для разработчиков Rails наиболее распространенными формами кэширования являются такие вещи, как мемоизация (описанная в предыдущей части этой серии статей о кэшировании), кэширование представлений (следите за новостями в следующей статье) и низкоуровневое кэширование, которое мы рассмотрим здесь.

Что такое низкоуровневое кэширование

То, что Rails называет кэшированием низкого уровня, на самом деле является просто чтением и записью данных в хранилище ключ-значение. По умолчанию Rails поддерживает хранилище в памяти, файлы в файловой системе и внешние хранилища, такие как Redis или memcached. Это называется «низкоуровневым» кэшированием, потому что вы имеете дело с объектом Rails.cache напрямую, сообщая ему, какое значение хранить и какой ключ использовать, в отличие от кэширования представлений, где Rails имеет встроенные вспомогательные методы для обработки этих мельчайших деталей. неприятные подробности для вас.

Наиболее распространенные варианты использования низкоуровневого кэширования, с которыми я сталкиваюсь, — это внешние запросы API только для чтения и тяжелые вычисления ActiveRecord. В случае с ActiveRecord есть несколько альтернатив кэшированию, описанных в первой части этой серии, которые вы, возможно, захотите изучить в первую очередь, поскольку внедрение кэширования также увеличивает сложность и количество ошибок в вашем приложении.

По умолчанию Rails отключает кэширование в процессе разработки, потому что вам обычно нужны свежие данные, когда вы работаете над функцией. Вы можете легко включать и выключать кэширование с помощью команды rails dev:cache.

Как это работает

Rails предоставляет три метода работы с кешем: read, write и fetch. Все они берут «ключ» кеша, и именно так мы ищем значение:

О read и write полезно знать, но при реализации низкоуровневого кэширования вы, вероятно, будете использовать метод fetch чаще всего.

fetch обеспечивает хорошую оболочку для чтения и записи. Вы передаете ему ключ и блок, и если значение этого ключа присутствует в кеше, оно будет возвращено, и блок не будет выполнен. Если для этого ключа нет кэшированного значения (или срок его действия истек, подробнее об истечении срока действия позже), он выполнит блок и сохранит результат в кэше для следующего раза.

Когда использовать низкоуровневое кэширование

Отличный вариант использования такого кэширования — это когда вы обращаетесь к внешнему API, чтобы получить значение, которое может меняться не так часто. В одном клиентском приложении у нас были некоторые расчеты, основанные на текущей фьючерсной цене некоторых товаров. Вместо обращения к API при каждом обновлении страницы мы кэшируем значение на определенный период времени (в нашем случае 10 минут).

Ключи и срок действия

Значение, которое вы передаете методу кеша ( read, write или fetch), является «ключом кеша», то есть key в паре ключ-значение, хранящейся в кеше. К тому времени, когда он попадет в хранилище кеша, это будет String, но Rails позволяет нам передавать и некоторые другие общие объекты:

  • Строка с любым содержимым, которое вам нравится
  • Символ
  • Объект, который отвечает на cache_key_with_version или cache_key (например, модель ActiveRecord, мы вскоре рассмотрим их)
  • Массив с любой комбинацией вышеперечисленного

Обычный метод, который я использовал при добавлении низкоуровневого кэширования в модель ActiveRecord, заключается в передаче массива, содержащего self (поэтому кэшированное значение привязано к текущему объекту) и имени метода в виде символа, например:

Чтобы увидеть, как будет выглядеть фактически сгенерированный ключ кэша, вы можете напрямую вызвать метод ActiveSupport:

Блок чисел здесь представляет собой комбинацию временных меток модели id и updated_at. Часть id предназначена для того, чтобы это кэшированное значение не перезаписывалось другими экземплярами модели. Временная метка update_at означает, что если модель обновляется, ключ автоматически изменяется, избавляя нас от необходимости вручную аннулировать кешированное значение.

Ранее я перечислил два метода генерации ключей кэша: cache_key и cache_key_with_version. ActiveRecord::Base реализует оба. cache_key_with_version имеет приоритет, включая отметку времени update_at, как показано выше. cache_key, с другой стороны, возвращает только название модели и id:

В старых версиях Rails кеширование допускало только cache_key, которое в моделях ActiveRecord включало бы метку времени. Изменение для разделения cache_key и cache_key_with_version было сделано в Rails 5.2, чтобы разрешить «перезапускаемые ключи кеша». Основная решаемая проблема заключается в следующем: каждый раз, когда изменяется временная метка модели updated_at, меняется ее ключ кэша. Это отлично подходит для аннулирования кеша, но означает, что теперь в кеше хранятся старые устаревшие значения, к которым мы больше никогда не получим доступ (поскольку мы никогда не сгенерируем старые ключи кеша).

Как видите, в кэше теперь хранятся две копии widget, даже несмотря на то, что старая копия больше никогда не будет найдена. В конце концов, кеш исчерпает свой лимит памяти и начнет отбрасывать старые значения, чтобы освободить место. В приложениях с большим количеством кэшированных данных это может означать удаление значений, которые мы все еще хотим кэшировать, но к которым обращаются реже.

Перезапускаемые ключи кеша решают эту проблему, позволяя нам явно передавать версию методу кеша. Базовый ключ, используемый в кеше, будет включать только идентификатор, а хранилище кеша будет проверять, соответствует ли версия, которую мы ему даем, тому, что хранится в кеше:

Трогательные модели

Бывают случаи, когда изменения в одной модели требуют изменений в связанной модели. Допустим, у вас есть модели Cart и Product для интернет-магазина, и если продукт обновляется, вам нужно обновить тележки. Здесь вы должны указать touch: true в отношениях:

Это означает, что любое изменение в Product автоматически изменит временные метки updated_at всех Carts, которым он «принадлежит». Это верно независимо от того, какие поля в Product обновляются, поэтому помните, что это приводит к некоторым накладным расходам, поскольку то, что раньше было одним вызовом базы данных для обновления продукта, теперь также включает обновление любого количества связанных Carts.

При необходимости вы также можете сами вызвать touch для модели, чтобы обновить временную метку, что может быть очень полезно для ручной инвалидации кеша через консоль Rails или если вам нужен более точный контроль над тем, какие именно строки обновляются.

Срок действия по времени

Один из вариантов, который вы можете передать методам кеша, — это когда вы хотите, чтобы эта запись «ключ-значение» была удалена. Лично я часто устанавливаю это значение на низкое число (или, что еще лучше, на переменную среды) при развертывании нового набора кода кэширования, так что, если что-то нужно настроить, вам не нужно делать много ручной аннулирования перед повторным тестированием.

Вы также можете установить значение срока действия по умолчанию для хранилища кеша.

Примеры использования

Я буду честен, почти все приложения, над которыми я работал, не нуждались в использовании этой формы кэширования, за одним важным исключением. Мы унаследовали приложение, которое, ну, сказать, что оно было плохо спроектировано, было бы преуменьшением.

Даже после значительной очистки нам нужно было решить две проблемы:

  1. Многие расчеты зависели от «текущей цены» товаров, полученных из API.
  2. Различные уровни вложенных агрегаций, таких как child.map { |c| c.computed_field }.sum, где computed_field содержит еще map{...}.sum

В идеальном мире #2 можно было бы решить, сводя эти вычисления к минимуму и заставляя базу данных выполнять обработку чисел. Однако консультационная работа всегда требует баланса между затратами разработчика и выгодой для клиента, и для ее завершения потребовалось бы нетривиальное количество часов, поэтому вместо этого мы нацелились на методы модели, которые вызывали основные проблемы с производительностью, и кэшировали их.

Затем это также связано с решением для #1; мы добавили запланированное задание, чтобы обновлять цену каждые 10 минут. Если цена изменилась, соответствующие модели будут touch изменены, что означает, что их кешированные расчеты будут признаны недействительными.

В качестве простого примера:

Гочки

Поскольку низкоуровневое кэширование Rails разработано с учетом временной метки ActiveRecord updated_at, код, использующий ее, может легко впасть в одну из двух крайностей:

  1. Кэшированное значение должно измениться, но update_at модели не изменилось (например, кэшируемый метод модели принимает аргумент), что приводит к ошибке инвалидации кеша.
  2. Свободное использование touch: true в ассоциациях ActiveRecord решает проблемы недействительности кеша, но вместо этого начинает сильно нагружать базу данных.

Дополнительным примечанием к #2 является то, что добавление большого количества параметров touch к объектам также может значительно увеличить количество записей в журнале базы данных. Я видел, как производственный сайт отключался просто из-за этой проблемы (т.е. на сервере базы данных не хватило места на жестком диске, хотя фактическая загрузка БД была нормальной).

Когда вид является узким местом

Здесь я в основном говорил о методах кэширования в рамках модели ActiveRecord, так как я считаю, что это наиболее распространенный вариант использования низкоуровневого кэширования в Rails. Однако Rails.cache можно вызывать из любого места вашего приложения Rails, поэтому нет никаких причин, по которым его нельзя использовать и внутри ваших классов бизнес-логики.

Вы можете вызвать его даже внутри представлений Rails, но если вы хотите кэшировать содержимое для уровня представления, в Rails есть встроенная поддержка этого, что мы и рассмотрим в следующей части этого руководства. ряд.

Первоначально опубликовано на https://www.honeybadger.io.