Урок 05

Тегирование кэша: Cache::tags()

Зачем теги

Проблема:

  • Много связанных ключей.
  • Ручной forget каждого — ошибки.
  • Инвалидация по сущности — сложно.

Что дают теги:

  • Группировку ключей.
  • Инвалидацию группы.
  • Связь данных с сущностью.

Правило: тег — логическая группа ключей.

Поддержка драйверов

Что поддерживает tags:

  • redis ✅
  • memcached ✅
  • array ✅
  • database ❌
  • file ❌
  • dynamodb ❌

Правило: tags — только redis/memcached/array.

Базовый синтаксис

Запись:

Cache::tags(['posts', 'homepage'])->put('posts:popular', $data, 3600);

Чтение:

$data = Cache::tags(['posts', 'homepage'])->get('posts:popular');

Очистка тега:

Cache::tags(['posts'])->flush();

Что важно: при чтении указывать те же теги.

remember с тегами

Cache::tags(['posts', 'homepage'])->remember('posts:popular', 3600, function () {
    return Post::popular()->limit(10)->get();
});

Правило: тег должен описывать ключ.

Инвалидация

Cache::tags(['posts'])->flush();

Что делает: удаляет все ключи с тегом posts.

Правило: flush по тегу — атомарная инвалидация группы.

Множественные теги

Ключ в двух тегах:

Cache::tags(['posts', 'user:5'])->put('user:5:posts', $posts, 3600);

Инвалидация:

// Все посты
Cache::tags(['posts'])->flush();

// Посты конкретного пользователя
Cache::tags(['user:5'])->flush();

Что даёт: ключ удаляется при flush любого из его тегов.

Правило: теги — связь между разными разрезами.

Пример: блог

Кэшируем посты:

Cache::tags(['posts', 'post:' . $post->id])->remember(
    "post:{$post->id}",
    now()->addHour(),
    fn () => $post->toArray(),
);

Инвалидация при сохранении:

static::saved(function (Post $post) {
    Cache::tags(['posts', 'post:' . $post->id])->flush();
    Cache::tags(['homepage'])->flush();
});

Что даёт:

  • Изменили пост → сбрасывается только он + связанные.
  • Общий тег posts — если нужен массовый сброс.

Пример: категории и товары

Товары категории:

Cache::tags(['products', 'category:' . $categoryId])->remember(
    "products:category:{$categoryId}",
    3600,
    fn () => Product::where('category_id', $categoryId)->get(),
);

Инвалидация при изменении категории:

Category::saved(function (Category $category) {
    Cache::tags(['category:' . $category->id])->flush();
});

Что даёт: изменил категорию — обновились её товары.

Пример: связь через внешние ключи

Комментарии к посту:

Cache::tags(['comments', 'post:' . $postId])->remember(
    "comments:post:{$postId}",
    600,
    fn () => Comment::where('post_id', $postId)->get(),
);

Изменение поста:

Cache::tags(['post:' . $post->id])->flush();

Что даёт: инвалидируется и пост, и его комментарии.

Правило: связывать сущности через тег {entity}:{id}.

Tags и flush

Cache::flush() — не сбрасывает теги.

Cache::tags([...])->flush() — сбрасывает только группу.

Правило: flush() без тегов — не использовать.

Вложенные теги

⚠️ Вложенных тегов нет.

Что делать: плоский список тегов.

Cache::tags(['posts', 'user:5', 'category:3'])->remember(...);

Правило: теги — плоские, но может быть много.

Ограничения Redis

Что важно:

  • Теги реализованы через множества.
  • С каждым ключом — связанные sets.
  • Много тегов — больше памяти.

Правило: не более 3–5 тегов на ключ.

Мониторинг

Что смотреть:

  • Размер множеств тегов.
  • Время flush.
  • Количество ключей на тег.

Через redis-cli:

redis-cli -n 1 SMEMBERS 'tag:posts:keys'

Правило: большой тег (10000+) — разбить.

Инвалидация через события модели

Post:

protected static function booted(): void
{
    static::saved(fn (Post $post) => self::forgetCache($post));
    static::deleted(fn (Post $post) => self::forgetCache($post));
}

protected static function forgetCache(Post $post): void
{
    Cache::tags(['posts', 'post:' . $post->id])->flush();
    Cache::forget('homepage:posts');
}

Правило: события модели — централизованная инвалидация.

Теги vs префиксы

Префикс: posts:popular — просто строка.

Тег: ['posts'] — метка.

Разница:

  • Префикс не помогает в flush.
  • Тег даёт групповой flush.

Правило: теги — только для групповой инвалидации.

Теги vs версия

Альтернатива: версия в ключе.

$v = Cache::get('posts:version', 1);
$key = "posts:v{$v}:popular";

Что даёт: при инкременте версии — старые ключи игнорируются.

Когда: драйвер не поддерживает теги.

Правило: версия — fallback для file/database.

Что кэшировать с тегами

Хорошо:

  • Списки сущностей.
  • Детали сущности.
  • Связанные данные.
  • Страницы с общим layout.

Плохо:

  • Глобальные агрегаты.
  • Персональные данные.

Правило: теги — по сущностям.

Практика

1. Проверьте драйвер — redis?

2. Оберните топ-3 запроса в tags.

3. Сделайте инвалидацию через модель.

4. Свяжите пост и комментарии тегом.

5. Проверьте flush тега.

6. Замерьте hit rate.

7. Сделайте тег homepage для главной.

8. Сделайте тег post:{id} для деталей.

9. Настройте алерт на размер тега.

10. Проверьте в redis-cli.

Частые ошибки

1. Теги на file/database:

Не работают

Решение: redis или memcached.

2. Забыли тег при чтении:

Не найдёт

Решение: всегда те же теги.

3. Слишком много тегов:

Память растёт

Решение: 3–5 на ключ.

4. Один тег на всё:

Flush убивает всё

Решение: гранулярные теги.

5. Теги без инвалидации:

Устаревшее

Решение: события модели.

6. flush() без тега:

Снесёт всё

Решение: только с тегами.

7. Вложенные теги:

Не поддерживаются

Решение: плоский список.

8. Не мониторить размер:

Redis OOM

Решение: алерт.

Итог

  • tags() — группа ключей.
  • Только redis / memcached / array.
  • remember — с тегами.
  • flush по тегу — атомарно.
  • {entity}:{id} — стандарт.
  • 3–5 тегов максимум.
  • Инвалидация через события модели.
  • Версия в ключе — для file/db.
  • Проверять через redis-cli.
  • Не использовать теги без инвалидации.

Дальше — инвалидация кэша.