Стратегии кэширования в Laravel

Источник: «Caching Strategies In Laravel»
При использовании кэширования обещание значительного повышения производительности должно быть сопоставлено с его стоимостью, связанной со сложностью и инфраструктурой. Давайте обсудим доступные варианты, чтобы вы могли использовать наилучшую реализацию для любой ситуации.

Я считаю кэширование одним из обоюдоострых мечей в разработке PHP-приложений.

С одной стороны, стратегии кэширования могут обеспечить значительный прирост производительности. С другой стороны, этот прирост может быть достигнут за счёт увеличения сложности кодовой базы или инфраструктуры. Кроме того, всегда существует угроза того, что кэширование предоставляет устаревшие данные.

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

Чтобы узнать больше об успешном решении кэширования для вашего приложения, давайте рассмотрим широкий спектр слоёв кэширования, доступных в экосистеме Laravel.

Общая стратегия кэширования

Приложения невероятно разнообразны по решаемым ими проблемам, поэтому не существует единого идеального варианта для каждого приложения. Чтобы решить, что подходит для конкретного приложения, необходимо хорошо понимать его бизнес-логику и используемые технологии.

Размышляя о внедрении/эффективности кэша, я придерживаюсь нескольких принципов.

Во-первых, оптимизируйте код

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

Итак, в этой статье мы предположим, что моё приложение уже достаточно оптимизировано.

Во-вторых, выберите слой/слои кэширования

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

Ниже приведён широкий и, несомненно, неполный список слоёв кэширования, доступных для большинства Laravel приложений.

DNS и веб-хост HTML кэширование

DNS и кэширование на уровне веб-хоста — это то, что полностью происходит перед нашими приложениями Laravel; обычно полный запрос кэшируется на основе запрашиваемого URL и заголовков. Такое кэширование может значительно снизить стоимость рендеринга HTML-страницы, включая все запросы, которые были необходимы для рендеринга этого HTML.

Такой стиль кэширования хорошо подходит для страниц, не требующих кастомизации на основе пользовательских данных, или для страниц, кастомизация которых может быть добавлена постфактум (например, с помощью JavaScript). Существует множество сервисов, которые могут управлять этим процессом, и у них есть широкий спектр моделей поведения, которые вы можете использовать. Некоторые основные примеры:

Эти сервисы отлично справляются с обслуживанием статического контента (подобно CDN), а также значительно снижают нагрузку на сервер, поскольку перехватывают входящий HTTP-запрос и возвращают ответ, не доводя его до моего веб-сервера/серверов. Меньшее количество запросов к серверу означает меньшую нагрузку и более быстрый цикл запроса/ответа.

Я могу настраивать эти кэши и их взаимодействие с моим приложением до невероятно тонких деталей. Если вам нужен пример, посмотрите эту статью, где оператор "Have I Been Pwned" Troy Hunt рассказывает о том, как они используют Cloudflare Cache Reserve.

Предостережения

Кэширование HTML в Laravel

Следующим уровнем является кеширование в вашем приложении Laravel на самом близком к пользователю уровне: HTML. Подобно кэшированию сервера и DNS, это кэширует весь HTML и позволяет во многих случаях избежать обращений к базе данных и других серверных вызовов.

Однако этот тип кэша даёт гораздо больше возможностей для контроля над тем, что кэшируется, а что нет. Например, я могу выбрать кэширование маршрутов на основе данных сессии или запроса. Я могу кэшировать кастомизированные HTML страницы для каждого пользователя, для каждого маршрута или для предоставленных GET-параметров.

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

Два основных пакета этого типа — Page Cache от Joseph Silber и ResponseCache от Spatie.

Предостережения

Встроенное кэширование Laravel

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

Laravel Cache

Инструментарий Laravel Cache предоставляет доступ к различным сервисам кэширования с помощью согласованного API. Этот слой кэширования позволяет кэшировать практически любое значение, хранящееся в месте, определённом строковым ключом — например, кэшировать 42 по ключу the-answer-to-life-the-universe-and-everything. Кэш Laravel также предлагает множество удобных методов для запоминания, истечения срока действия, разрушение, блокировки и регулирования кэша.

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

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

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

Предостережения

Мемоизация

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

Мемоизация в PHP классе

Мемоизация ресурсоёмких вычислений в объекте класса PHP очень проста:

class MyPhpClass
{
protected $memoizedVariable;

public function getResourceHeavyValue()
{
if ($this->memoizedVariable !== null) {
return $this->memoizedVariable;
}

$this->memoizedVariable = $this->doingSomeHeavyCalculation();

return $this->memoizedVariable;
}
}

Прелесть этого стиля в том, что это просто шаблон кода. Пока я использую один и тот же экземпляр объекта MyPhpClass, я могу вызывать $object->getResourceHeavyValue() сколько угодно раз в этом цикле запроса, и он не сделает ни одного вычисления снова.

Предостережения
Мемоизация во всем приложении

Такая мемоизация также работает только для текущего цикла запросов, но в масштабах всего приложения. Once — надёжный пакет для этого, написанный Taylor Otwell и выпущенный Spatie с его разрешения.

Вы можете вызвать функцию once() и передать ей замыкание. И что бы ни происходило в этом замыкании, оно будет выполнено только один раз в этом запросе, независимо от того, сколько раз будет вызван этот фрагмент кода.

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

Кэширование запросов базы данных

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

Этот механизм кэширования на стороне базы данных может существовать либо в программном интерфейсе базы данных (т.е. перед фасадами DB и Model), либо перед базой данных.

Наиболее распространённый паттерн для кэширования запросов к базе данных — это использование кэша Laravel, о котором мы уже говорили, для обёртывания сложных вызовов БД или модели и кэширования результатов.

$states = Cache::remember('states', $seconds = 3600, function () {
return State::all();
});

Существуют также сервисы кэширования запросов к базе данных, выполняющие кэширование непосредственно перед базой данных. PlanetScale Boost предлагает платный сервис кэширования запросов к базе данных, обратитесь к ним за более подробной информацией и деталями реализации, специфичными для Laravel!

В-третьих, не забывайте очищать кэши.

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

Вот картина последствий плохого удаления кэша:

Письма будут отправляться не по назначению. Будут предлагаться ложные поздравления. Пользователи будут дважды импортировать транзакции, потому что график не обновился после того, как они впервые ввели транзакцию. Пользователи будут платить дважды или платить неправильную сумму. Все получат ваше приложение бесплатно.

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

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

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

Всякий раз, когда я добавляю кэширование, я выделяю время для выполнения этих задач:

  1. Добавьте кэширование
  2. Документируйте слой кэширования: как настроить его в локальной, тестовой и продакшн средах и как он должен работать.
  3. Добавьте разрушение кэша и/или истечение срока действия кэша. Добавьте тестирование, подтверждающее, что разрушение происходит.

Разрушение кэша важно в любом случае, когда я определяю стандартизированный ключ для хранения. Рассмотрим такую часть данных, как sales-this-week. Если бы я отслеживал продажи пользователя на этой неделе, я мог бы хранить это значение в кэше с ключом users.[$user->id].sales-this-week. Это упрощает поиск только этого пользователя, или только его продаж на этой неделе, или всех данных, связанных с пользователем.

Одно из решений разрушения кэша: Вытеснение кэша

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

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

Я могу использовать вытеснение кэша в этой стратегии, создавая ключи, включающие временную метку, например users.[$user->id].[$user->updated_at].sales-this-week. Я сделаю так, что каждый раз, когда происходит продажа у пользователя, я $user->touch() пользователя, ответственного за продажу, что приведёт к обновлению временной метки updated_at. В следующий раз, когда я попытаюсь получить данные о продажах этого пользователя, поиск sales-this-week не найдёт совпадения и вычислит и кэширует новое значение sales-this-week.

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

Если я не хочу настраивать драйвер кэша на вытеснение кэша, я могу в планировщике Laravel запустить php artisan cache:clear для драйвера или тегов, которые я хотел бы очищать с разумной периодичностью.

Спасибо Tony за напоминание, что существуют политики вытеснения кэша.

Разделение постоянных и изменяемых кэшей данных

Требования к жизненному циклу данных могут побудить использовать различные механизмы кэширования.

Рассмотрим приложение, использующее внешний сервис для получения GPS-координат адресов, введённых в систему. Если местоположение адреса не может быть сопоставлено, приложение кэширует эту адресную строку в ignore-list. Оно больше не будет выполнять вызов API для этого адреса к этой внешней службе.

Более эффективным решением является кэширование достоверных данных, полученных от службы GPS. Редко когда адрес меняет своё GPS-положение. В следующий раз, когда я попытаюсь найти тот же адрес, я смогу использовать локальное кэшированное значение! Таким образом, я сэкономлю и время HTTP-запроса, и затраты на использование внешнего сервиса.

В обоих приведённых выше примерах я не хочу, чтобы эти данные уничтожались, когда я пишу php artisan cache:clear. Это указывает на то, что мне нужен более постоянный кэш данных для этих пар ключ/значение. Возможно, стоит использовать другой драйвер кэша, который я никогда не очищаю, или постоянный кэш на отдельном сервере. Если жизненный цикл этих данных может превышать продолжительность жизненного цикла сервера приложения, то наиболее целесообразно хранить эти данные в постоянном хранилище данных, например в базе данных приложения.

Заключение

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

В одном из следующих постов я расскажу о некоторых паттернах кодирования, которые делают использование Мемоизации и Cache более удобным.

Дополнительные материалы

Предыдущая Статья

Основы TypeScript: Объединение, Литеральные и Размеченные типы

Следующая Статья

Основы TypeScript: Типизация функций и сигнатур