» » » Кеширование в Laravel: основы плюс tips&tricks

 

Кеширование в Laravel: основы плюс tips&tricks

Автор: admin от 13-08-2019, 18:50, посмотрело: 24

Техника кеширования позволяет создавать более масштабируемые приложения, запоминания результаты некоторых запросов в быстрое in-memory хранилище. Однако, некорректно реализованное кеширование может сильно ухудшить впечатление пользователя о вашем приложении. Эта статья содержит некоторые базовые понятия о кешировании, различные правила и табу, которые я извлек из нескольких прошлых своих проектов.



Не используйте кеширование.



Ваш проект работает быстро и не имеет никаких проблем с производительностью?

Забудьте о кешировании. Серьезно :)

Оно сильно усложнит операции чтения из базы без каких-либо бенефитов.



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

глава Cache, которая обьясняет как установить необходимые драйверы для вашего приложения и главный функционал.



Данные в кеше



Все стандартные драйверы Laravel хранят данные как строки. Когда мы просим сохранить в кеше экземпляр модели Eloquent, оно использует функцию serialize, чтобы получить строку из обьекта. Функция unserialize восстанавливает состояние обьекта когда мы получаем его из кеша.



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

Сущности Eloquent и коллекции легко могут быть закешированы и являются самыми популярными значениями в кеше приложений Laravel. Однако, использование других типов тоже практикуется довольно широко. Метод Cache::increment популярен для реализации различных счетчиков. Также, атомарные локи весьма полезны когда разработчики сражаются с race conditions.



Что кешировать?



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



Обычно, выборка сущностей по id, используя Model::find($id) работает очень быстро, но если эта таблица сильно загружена многочисленными запросами update, insert и delete, уменьшение количества select запросов даст хорошую передышку базе данных. Сущности с отношениями hasMany, которые будут загружаться каждый раз, тоже хорошие кандидаты на кеширование. Когда я работал на проекте с 10+ миллионов посетителей в день мы кешировали почти любой select запрос.



Инвалидация кеша



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



Пользователь: Я обновил публикацию, но продолжаю видеть старую версию!
Разработчик: Пожалуйста, подождите 15 минут(или полчаса, или час)...  


Это поведение весьма неудобно для пользователей и очевидное решение удалять из кеша старые данные, когда мы их обновили быстро приходит в голову. Этот процесс называется инвалидацией. Для простых ключей типа "post_%id%", инвалидация не очень сложная.

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



abstract class PostEvent
{
    /** @var Post */
    private $post;

    public function __construct(Post $post) {
        $thispost = $post;
    }

    public function getPost(): Post {
        return $thispost;    
    }
}

final class PostSaved extends PostEvent{}
final class PostDeleted extends PostEvent{} 


Разумеется, по PSR-4, каждый класс должен лежать в своем файле. Настраиваем Post Eloquent класс (используя документацию):



class Post extends Model
{
    protected $dispatchesEvents = [
        'saved' => PostSaved::class,
        'deleted' => PostDeleted::class,
    ];
}


Создаем слушатель этих событий:



class EventServiceProvider extends ServiceProvider 
{
    protected $listen = [
        PostSaved::class => [
            ClearPostCache::class,
        ],
        PostDeleted::class => [
            ClearPostCache::class,
        ],
    ];
}

class ClearPostCache
{
    public function handle(PostEvent $event)
    {
        Cache::forget('post_' . $eventgetPost()id);
    }
}


Этот код будет удалять закешированные значения после каждого обновления или удаления сущностей Post. Инвалидация списков сущностей, таких как top-5 статей или последних новостей, будет чуток посложнее. Я видел три стратегии:



Стратегия "Не инвалидируем"



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



Стратегия "Найти и обезвредить"



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



public function getTopPosts()
{
    return Cache::remember('top_posts', 900, function() {
        return Post::/*формируем запрос получения top-5*/()get();
    });
}

class CheckAndClearTopPostsCache
{
    public function handle(PostEvent $event)
    {
        $updatedPost = $eventgetPost();

        $posts = Cache::get('top_posts', []);
        foreach($posts as $post) {
            if($updatedPostid == $postid) {
                Cache::forget('top_posts');
                return;
            }
        }
    }
}


Выглядит уродливо, зато работает.



Стратегия "хранить id"



Если порядок элементов в списке неважен, то в кеше можно хранить только id записей. После получения id, можно сформировать список ключей вида 'post_'.$id и получить все значения используя метод Cache::many, который достает много значений из кеша за один запрос (это еще называется multi get).



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



Кеширование отношений



Кеширование сущностей с отношениями требует повышенного внимания.



$post = Post::findOrFail($id);
foreach($postcomments...)


Этот код выполняет два SELECT запроса. Получение сущности по id и комментариев по post_id. Реализуем кеширование:



public function getPost($id): Post
{
    return Cache::remember('post_' . $id, 900, function() use ($id) {
        return Post::findOrFail($id);
    });
}

$post = getPost($id);
foreach($postcomments...)


Первый запрос был закеширован, а второй — нет. Когда драйвер кеша записывает Post в кеш, comments еще не загружены. Если мы хотим кешировать и их тоже, то мы должны загрузить их вручную:



public function getPost($id): Post
{
    return Cache::remember('post_' . $id, 900, function() use ($id) {
        $post = Post::findOrFail($id);
        $postload('comments');
        return $post;
    });
}


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



public function getPostComments(Post $post)
{
    return Cache::remember('post_comments_' . $postid,  900,  
        function() use ($post) {
            return $postcomments;
        });
}

$post = getPost($id);
$comments = getPostComments($post);

foreach($comments...)


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



Single source of truth для ключей кеширования



Если на проекте реализована инвалидация, ключи кеширования генерируются как минимум в двух местах: для вызова Cache::get / Cache::remember и для вызова Cache::forget. Я уже встречался с ситуациями, когда этот ключ был изменен в одном месте, но не в другом и инвалидация ломалась. Обычный совет для таких случаев — константы, но ключи кеширования формируются динамически, поэтому я использую специальный классы, генерирующие ключи:



final class CacheKeys
{   
    public function postById($id): string {
        return 'post_' . $id;
    }

    public function postComments($postId): string {
        return 'post_comments' . $id;
    }
}

Cache::remember(CacheKeys::postById($id), 900, function() use ($id) {
    $post = Post::findOrFail($id);
});

// ....

Cache::forget(CacheKeys::postById($id));


Время жизни ключей также можно вынести в константы, ради лучшей читаемости. Эти 900 или 15*60 увеличивают когнитивную нагрузку при чтении кода.



Не используйте кеш в операциях записи



При реализации операций записи, таких как изменение заголовка или текста публикации, велик соблазн использовать метод getPost, написанный ранее:



$post = getPost($id);
$posttitle = $newTitle;
$postsave();


Пожалуйста, не делайте так. Значение в кеше может быть устаревшим, даже если инвалидация сделана корректно. Небольшой race condition и публикация потеряет значения, сделанные другим пользователем. Оптимистические блокировки помогут хотя бы не потерять изменения, но количество ошибочных запросов может сильно возрасти.



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



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



Источник: Хабр / Интересные публикации

Теги: laravel cache

Категория: Google

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *