Backend-разработка

PostgreSQL + Redis: стратегии кэширования для высоконагруженных Laravel-приложений

Ruslan Ismailov Опубликовано 10 мин чтения
P

Введение: зачем нужно кэширование и когда PostgreSQL перестаёт справляться

PostgreSQL — одна из наиболее надёжных и функциональных реляционных СУБД. Она прекрасно справляется с транзакционной нагрузкой, сложными JOIN-запросами и большими объёмами данных. Но даже самая хорошо настроенная база данных имеет предел: при нескольких сотнях запросов в секунду к тяжёлым аналитическим выборкам время ответа начинает расти, а CPU-утилизация сервера стремится к 100%.

Типичные симптомы, при которых пора внедрять кэширование:

  • Время выполнения ключевых запросов превышает 100–200 мс даже при наличии индексов.

  • Один и тот же запрос выполняется десятки раз в секунду с одинаковыми параметрами.

  • PostgreSQL-сервер становится узким местом по I/O или CPU при пиковых нагрузках.

  • Масштабирование базы данных горизонтально затруднено или слишком дорого.

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

Архитектурная роль Redis рядом с PostgreSQL: паттерны кэширования

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

Cache-Aside (Lazy Loading)

Наиболее распространённый паттерн в Laravel-приложениях. Логика проста: приложение сначала проверяет кэш, и только при промахе (cache miss) обращается к PostgreSQL, сохраняя результат в Redis.

$products = Cache::remember('products.catalog.page.'.$page, 3600, function () use ($page) {
    return Product::with('category')
        ->where('is_active', true)
        ->orderBy('created_at', 'desc')
        ->paginate(20, ['*'], 'page', $page);
});

Плюсы: простота реализации, кэшируются только реально запрашиваемые данные.
Минусы: первый запрос всегда идёт в базу (cold start), возможно кратковременное устаревание данных.

Write-Through

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

public function updateProduct(int $id, array $data): Product
{
    $product = Product::findOrFail($id);
    $product->update($data);
    
    // Синхронно обновляем кэш
    Cache::put('product.'.$id, $product->fresh(), 3600);
    Cache::tags(['products'])->flush();
    
    return $product;
}

Read-Through

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

Для большинства Laravel-проектов рекомендуется начинать с Cache-Aside как наиболее гибкого подхода, переходя к Write-Through для критически важных справочных данных.

Настройка Redis в Laravel: конфигурация, драйверы, phpredis vs predis в 2026 году

Установка и базовая конфигурация

В 2026 году для продакшн-окружений однозначно рекомендуется расширение phpredis вместо пакета predis. phpredis реализован на C, работает значительно быстрее и потребляет меньше памяти. Predis остаётся вариантом только при невозможности установки PHP-расширений (например, в некоторых managed-хостингах).

Установка phpredis:

# Ubuntu/Debian
apt install php-redis

# или через PECL
pecl install redis

В config/database.php настройте подключение:

'redis' => [
    'client' => env('REDIS_CLIENT', 'phpredis'),

    'default' => [
        'host'     => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port'     => env('REDIS_PORT', 6379),
        'database' => env('REDIS_DB', 0),
    ],

    'cache' => [
        'host'     => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port'     => env('REDIS_PORT', 6379),
        'database' => env('REDIS_CACHE_DB', 1), // отдельная БД для кэша
    ],
],

В .env:

CACHE_DRIVER=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_CACHE_DB=1

Разделение баз данных Redis

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

Кэширование запросов к PostgreSQL через Laravel Cache: теги, TTL, инвалидация

Теги кэша (Cache Tags)

Cache Tags — мощный инструмент для групповой инвалидации. Вместо того чтобы вручную удалять каждый ключ, достаточно сбросить весь тег.

// Сохранение с тегами
$users = Cache::tags(['users', 'admin'])->remember(
    'users.admin.list',
    now()->addHour(),
    fn() => User::role('admin')->with('permissions')->get()
);

// Инвалидация всего тега при изменении пользователя
public function boot(): void
{
    User::saved(function (User $user) {
        Cache::tags(['users'])->flush();
    });
}

Важно: Cache Tags работают только с драйверами Redis и Memcached. При использовании file или database драйвера они недоступны.

Гибкое управление TTL

Устанавливайте TTL исходя из частоты изменения данных:

  • Справочники (категории, настройки): 24 часа и более.

  • Каталог товаров: 1–4 часа.

  • Агрегированная статистика: 5–15 минут.

  • Пользовательские данные: 1–30 минут в зависимости от требований к актуальности.

Инвалидация через Model Events

Автоматическая инвалидация при изменении модели — надёжный способ избежать устаревших данных:

// app/Observers/ProductObserver.php
class ProductObserver
{
    public function saved(Product $product): void
    {
        Cache::tags(['products'])->flush();
        Cache::forget('product.'.$product->id);
    }

    public function deleted(Product $product): void
    {
        Cache::tags(['products'])->flush();
        Cache::forget('product.'.$product->id);
    }
}

// app/Providers/AppServiceProvider.php
Product::observe(ProductObserver::class);

Кэширование сессий и очередей в Redis

Сессии в Redis

Перенос сессий из файловой системы или PostgreSQL в Redis даёт ощутимый прирост производительности при горизонтальном масштабировании. Все ноды приложения получают доступ к единому хранилищу сессий без синхронизации файлов.

// config/session.php
'driver'     => env('SESSION_DRIVER', 'redis'),
'lifetime'   => env('SESSION_LIFETIME', 120),
'connection' => 'session', // отдельное подключение Redis

Очереди на Redis

Redis Queue в Laravel обеспечивает высокую пропускную способность для фоновых задач. По сравнению с database-драйвером очередей, Redis работает на порядок быстрее за счёт атомарных операций со списками.

// .env
QUEUE_CONNECTION=redis

// dispatch задачи
ProcessOrderJob::dispatch($order)->onQueue('orders');

// запуск воркеров
php artisan queue:work redis --queue=orders,default --tries=3 --timeout=60

Практический пример: кэширование сложного запроса к PostgreSQL с автоматической инвалидацией

Рассмотрим реальный сценарий: дашборд с агрегированной статистикой продаж, который включает несколько JOIN-ов и window functions в PostgreSQL.

// app/Services/SalesDashboardService.php
class SalesDashboardService
{
    private const CACHE_TTL = 900; // 15 минут
    private const CACHE_TAG = 'sales_dashboard';

    public function getSalesStats(int $userId, string $period): array
    {
        $cacheKey = "sales.stats.{$userId}.{$period}";

        return Cache::tags([self::CACHE_TAG, 'user.'.$userId])
            ->remember($cacheKey, self::CACHE_TTL, function () use ($userId, $period) {
                return DB::select(
                    "SELECT
                        DATE_TRUNC(:period, o.created_at) AS period_date,
                        COUNT(o.id) AS orders_count,
                        SUM(o.total_amount) AS revenue,
                        AVG(o.total_amount) AS avg_order_value,
                        RANK() OVER (ORDER BY SUM(o.total_amount) DESC) AS revenue_rank
                    FROM orders o
                    INNER JOIN order_items oi ON oi.order_id = o.id
                    WHERE o.user_id = :user_id
                        AND o.created_at >= NOW() - INTERVAL '1 year'
                        AND o.status = 'completed'
                    GROUP BY DATE_TRUNC(:period, o.created_at)
                    ORDER BY period_date DESC",
                    ['period' => $period, 'user_id' => $userId]
                );
            });
    }

    public function invalidateUserCache(int $userId): void
    {
        Cache::tags(['user.'.$userId])->flush();
    }
}

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

// app/Observers/OrderObserver.php
class OrderObserver
{
    public function __construct(
        private SalesDashboardService $dashboardService
    ) {}

    public function saved(Order $order): void
    {
        // Инвалидируем только кэш конкретного пользователя
        $this->dashboardService->invalidateUserCache($order->user_id);
        
        // Также сбрасываем общую статистику
        Cache::tags([SalesDashboardService::CACHE_TAG])->flush();
    }
}

Такая архитектура позволяет снизить нагрузку на PostgreSQL при повторных запросах к дашборду с ~150 мс до ~2 мс за счёт обслуживания из Redis.

Мониторинг Redis и PostgreSQL: как выявлять узкие места

Мониторинг Redis

Ключевые метрики Redis, за которыми нужно следить:

  • hit_rate (keyspace_hits / (keyspace_hits + keyspace_misses)) — должен быть выше 80–90% в устоявшейся системе.

  • used_memory — не допускайте превышения maxmemory, иначе Redis начнёт вытеснять ключи по политике eviction.

  • connected_clients — аномальный рост может указывать на утечку соединений.

  • latency — среднее время выполнения команд, норма < 1 мс.

# Мониторинг через redis-cli
redis-cli INFO stats | grep keyspace
redis-cli INFO memory | grep used_memory_human
redis-cli MONITOR  # осторожно: высокая нагрузка на сам Redis
redis-cli --latency -h 127.0.0.1

Для продакшна рекомендуется интеграция с Prometheus + Redis Exporter или использование Laravel Telescope для отслеживания cache hits/misses на уровне приложения.

Мониторинг PostgreSQL

При работе в связке с Redis важно убедиться, что кэш реально снижает нагрузку на PostgreSQL:

-- Топ медленных запросов (требует pg_stat_statements)
SELECT query,
       calls,
       mean_exec_time,
       total_exec_time,
       rows
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- Активные соединения
SELECT count(*), state
FROM pg_stat_activity
GROUP BY state;

Если после внедрения кэширования calls для тяжёлых запросов не снизился — инвалидация кэша происходит слишком часто или ключи формируются неправильно.

Laravel Telescope и Debugbar

В режиме разработки Laravel Telescope даёт детальную информацию о каждом обращении к Redis и PostgreSQL: какие ключи читаются, сколько cache miss-ов происходит, время выполнения запросов. Это незаменимый инструмент для отладки стратегии кэширования.

Заключение: когда кэш помогает, а когда мешает

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

Кэш помогает, когда:

  • Данные читаются значительно чаще, чем изменяются.

  • Вычисление или получение данных из PostgreSQL занимает заметное время.

  • Одинаковые запросы выполняются многократно с теми же параметрами.

  • Требуется горизонтальное масштабирование без увеличения нагрузки на СУБД.

Кэш мешает, когда:

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

  • Логика инвалидации сложна и запутана — высок риск показывать устаревшие данные.

  • Размер кэшируемых данных огромен при ограниченной памяти Redis — eviction будет снижать hit rate.

  • Запросы всегда уникальны (например, сложные фильтры с большим числом комбинаций) — кэш практически никогда не будет попадать в hit.

Правильная стратегия кэширования в Laravel-приложениях строится итерационно: сначала измерьте реальные узкие места через pg_stat_statements и Laravel Telescope, затем кэшируйте только то, что действительно нагружает PostgreSQL, и тщательно продумайте инвалидацию. Связка PostgreSQL + Redis при грамотном применении позволяет обслуживать нагрузку, которая в 10–50 раз превышает возможности системы без кэширования.

Технологии

Теги

Руслан Исмаилов

Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →