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

Эта статья была первоначально написана Эшли Алленом в блоге разработчиков Honeybadger.

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

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

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

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

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

Преимущества использования кэширования

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

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

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

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

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

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

Хранение данных в кэше

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

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

public function getUsers($foo, $bar)
{
    return User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();
}
Вход в полноэкранный режим Выйти из полноэкранного режима

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

public function getUsers($foo, $bar)
{
    $users = User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();

    Cache::forever("user_query_results_foo_{$foo}_bar_{$bar}", $users);

    return $user;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

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

Так, если бы мы хотели кэшировать вышеприведенный запрос пользователей в течение 10 минут, мы могли бы использовать метод put():

public function getUsers($foo, $bar)
{
    $users = User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();

    Cache::put("user_query_results_foo_{$foo}_bar_{$bar}", $users, now()->addMinutes(10));

    return $users;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Получение данных из кэша

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

public function getUsers($foo, $bar)
{
    $cacheKey = "user_query_results_foo_{$foo}_bar_{$bar}";

    if (Cache::has($cacheKey)) {
        return Cache::get($cacheKey);
    }

    $users = User::query()
        ->where('foo', $foo)
        ->where('bar', $bar)
        ->get();

    Cache::put($cacheKey, $users, now()->addMinutes(10));

    return $users;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

public function getUsers($foo, $bar)
{
    return Cache::remember("user_query_results_foo_{$foo}_bar_{$bar}", now()->addMinutes(10), function () use ($foo, $bar) {
        return User::query()
            ->where('foo', $foo)
            ->where('bar', $bar)
            ->get();
    });
}
Войти в полноэкранный режим Выход из полноэкранного режима

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

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

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

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

public function getUsers($foo, $bar)
{
    return Cache::store('array')->remember("user_query_results_foo_{$foo}_bar_{$bar}", function () use ($foo, $bar) {
        return User::query()
            ->where('foo', $foo)
            ->where('bar', $bar)
            ->get();
    });
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Как видно из примера, мы использовали store('array') для выбора драйвера кэша array.

Использование команд кэширования Laravel

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

php artisan route:cache
Войти в полноэкранный режим Выйти из полноэкранного режима

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

php artisan route:clear
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

php artisan config:cache
Войти в полноэкранный режим Выйти из полноэкранного режима

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

php artisan config:clear
Войти в полноэкранный режим Выйти из полноэкранного режима

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

php artisan event:cache
Войти в полноэкранный режим Выйти из полноэкранного режима
php artisan view:cache
Войти в полноэкранный режим Выйти из полноэкранного режима

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

php artisan event:clear
Войти в полноэкранный режим Выйти из полноэкранного режима
php artisan view:clear
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

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

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

Кроме того, если вы добавляете кэширование в приложение Laravel, это часто означает, что вам нужно добавить кэширующий сервер, например Redis, в инфраструктуру вашего приложения. Использование такого подхода может создать дополнительный вектор атаки, а также открыть дополнительный порт на сервере(ах), но если безопасность инфраструктуры вашего приложения настроена хорошо, это не должно стать проблемой. Тем не менее, об этом следует помнить, особенно если вы кэшируете персонально идентифицируемую информацию (PII), поскольку вы не хотите рисковать утечкой кэшированных данных в результате атаки.

Заключение

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

Оставьте комментарий

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