Урок 04

Кэш запросов к БД: Cache::remember()

Зачем кэшировать запросы

Проблема:

  • БД — главный bottleneck.
  • Одинаковые SELECT повторяются.
  • Агрегаты пересчитываются.
  • Тяжёлые JOIN на каждый запрос.

Что даёт кэш:

  • Меньше запросов в 5–50×.
  • Быстрее ответы.
  • Меньше CPU и I/O БД.

Правило: любой тяжёлый SELECT — кандидат.

remember()

Главный метод:

$value = Cache::remember('key', $ttl, function () {
    return DB::table('users')->count();
});

Что делает:

  1. GET из кэша.
  2. Есть → возвращает.
  3. Нет → выполняет closure.
  4. PUT результат в кэш.
  5. Возвращает значение.

Правило: remember() — 90% сценариев кэша данных.

rememberForever()

Без TTL:

$value = Cache::rememberForever('key', fn () => Config::all());

Когда: данные меняются только при деплое.

⚠️ Без инвалидации — вечно устаревшие.

Правило: rememberForever — только для статики.

flexible()

Что это: новый метод Laravel.

$value = Cache::flexible('key', [60, 300], function () {
    return Post::popular()->get();
});

Что означает:

  • 60 сек — свежий.
  • 300 сек — можно отдать устаревший (background refresh).

Что даёт:

  • Мгновенный ответ.
  • Обновление в фоне.

Правило: flexible — для часто читаемых данных.

Ключи кэша

Плохой ключ:

Cache::remember('users', 3600, fn () => User::all());

Почему плохо:

  • Нет параметров.
  • Коллизии между разными выборками.

Хороший ключ:

Cache::remember("users:active:page:{$page}", 3600, function () use ($page) {
    return User::where('active', true)->paginate(20, ['*'], 'page', $page);
});

Правило: в ключе — все параметры выборки.

Соглашение по ключам

Формат:

{сущность}:{версия}:{параметры}

Примеры:

  • users:active:page:1
  • posts:slug:hello-world
  • stats:daily:2026-10-16
  • menu:header:v3

Правило: единый формат по проекту.

Параметры в ключе

Через строку:

$key = 'products:' . md5(json_encode($filters));

Через хэш:

$key = 'catalog:' . md5(serialize([$categoryId, $sort, $page]));

Что важно: избегать длинных ключей (ограничение Redis ~512 байт).

Правило: хэшировать длинные наборы параметров.

Пример: топ-N

public function popular(int $limit = 10): Collection
{
    return Cache::remember(
        "posts:popular:{$limit}",
        now()->addMinutes(15),
        fn () => Post::withCount('views')
            ->orderByDesc('views_count')
            ->limit($limit)
            ->get(),
    );
}

Что даёт: топ-10 не считая каждый раз.

Правило: топ-N — кэшировать всегда.

Пример: справочник

public function all(): Collection
{
    return Cache::remember(
        'categories:all',
        now()->addDay(),
        fn () => Category::orderBy('name')->get(),
    );
}

Что даёт: справочник — раз в сутки.

Правило: справочники — долгий TTL.

Пример: агрегат

public function dailyStats(Carbon $date): array
{
    return Cache::remember(
        "stats:daily:{$date->toDateString()}",
        now()->addHours(2),
        function () use ($date) {
            return [
                'orders' => Order::whereDate('created_at', $date)->count(),
                'revenue' => Order::whereDate('created_at', $date)->sum('total'),
                'users' => User::whereDate('created_at', $date)->count(),
            ];
        },
    );
}

Что даёт: один тяжёлый расчёт вместо трёх.

Правило: агрегаты — группировать в один cache-hit.

Пример: внешний API

public function rate(string $currency): float
{
    return Cache::remember(
        "currency:{$currency}",
        now()->addHour(),
        fn () => Http::get("https://api.fx.com/{$currency}")->json('rate'),
    );
}

Что даёт: внешний API не вызывается чаще раза в час.

Правило: любой внешний API — через кэш.

add() и put()

put() — записать:

Cache::put('key', $value, 3600);
Cache::put('key', $value, now()->addHour());

add() — только если нет:

Cache::add('lock:job', true, 60);

Правило: add — для идемпотентности.

get() с дефолтом

$value = Cache::get('key', fn () => 'default');
$value = Cache::get('key', 'default');

Что даёт: безопасный fallback.

has() и missing()

if (Cache::has('key')) { /* ... */ }
if (Cache::missing('key')) { /* ... */ }

Правило: missing — читается лучше.

forget() и flush()

Удалить ключ:

Cache::forget('posts:popular:10');

Удалить всё:

Cache::flush();

⚠️ flush на общем Redis — убивает всё, включая сессии.

Правило: flush — не в проде.

Инкремент и декремент

Cache::increment('views:' . $post->id);
Cache::decrement('stock:' . $product->id);
Cache::increment('views:' . $post->id, 5);

Что даёт: атомарные счётчики.

Правило: счётчики — в Redis, не в БД.

Pull

$value = Cache::pull('token:' . $user->id);

Что делает: get + forget одним вызовом.

Когда: одноразовые токены.

TTL

Варианты:

Cache::put('key', $v, 3600);           // секунды
Cache::put('key', $v, now()->addHour()); // Carbon
Cache::put('key', $v, null);           // forever

Правило: TTL — всегда явно, не полагаться на дефолт.

Дефолтный TTL

config/cache.php:

'ttl' => env('CACHE_TTL', 3600),

Что важно: дефолт — явный для проекта.

Нагрев кэша

Проблема: первый запрос медленный.

Что делать: прогреть заранее.

Artisan-команда:

class WarmCache extends Command
{
    protected $signature = 'cache:warm';

    public function handle(): void
    {
        Cache::remember('posts:popular:10', now()->addHour(),
            fn () => Post::popular()->limit(10)->get());

        Cache::remember('categories:all', now()->addDay(),
            fn () => Category::all());
    }
}

Когда: после деплоя, в scheduler.

Правило: нагрев — после optimize:clear.

Декремент нагрузки

Что ускоряется:

  • Топ-N — 10–100×.
  • Справочники — 50×.
  • Агрегаты — 20×.
  • Внешние API — 100×.

Правило: кэш запросов — самый быстрый win.

Что НЕ кэшировать

  • Личный кабинет.
  • Токены в общем ключе.
  • Данные с гарантированной свежестью.
  • Персональные списки.

Правило: если сомневаешься — не кэшируй.

Метрики

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

  • Hit rate по ключу.
  • Размер значения.
  • TTL попаданий.
  • Время до первого miss.

Через Pulse: cache hit rate в UI.

Правило: каждый важный ключ — в мониторинге.

Практика

1. Найдите 5 тяжёлых SELECT.

2. Оберните каждый в remember().

3. Выберите осмысленные ключи.

4. Задайте TTL.

5. Замерьте до/после.

6. Настройте инвалидацию.

7. Сделайте cache:warm.

8. Прогоните в scheduler.

9. Проверьте Pulse hit rate.

10. Убедитесь: нет персональных данных в общих ключах.

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

1. Один ключ для разных фильтров:

Коллизия

Решение: параметры в ключе.

2. Долгий TTL на живое:

Устаревшее

Решение: TTL по частоте изменения.

3. Кэш без инвалидации:

Старьё

Решение: слушатели модели.

4. rememberForever на динамике:

Вечно устаревшее

Решение: TTL.

5. Personal data под общим ключом:

Утечка

Решение: user_id в ключе.

6. Не греть кэш:

Первый запрос медленный

Решение: cache:warm.

7. Cache::flush() в проде:

Снос сессий

Решение: forget по ключам.

8. Кэш объектов без Serializable:

Ошибка

Решение: массивы или implements.

Итог

  • remember() — основа.
  • rememberForever — только статика.
  • flexible — фон-refresh.
  • Ключ — все параметры.
  • TTL — явно.
  • Справочники, топ-N, агрегаты, API — кэшировать.
  • Инвалидация — через слушатели.
  • Прогрев — cache:warm.
  • Метрики — Pulse.
  • Не кэшировать персональное.

Дальше — тегирование кэша: Cache::tags().