PostgreSQL + Redis: стратегии кэширования для высоконагруженных Laravel-приложений
Введение: зачем нужно кэширование и когда 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. Подробнее обо мне →