Запити до бази даних — це перше, на що варто звернути увагу, коли ваш застосунок на Laravel починає працювати повільно. Кожного разу, коли користувач завантажує сторінку, система може неодноразово звертатися до бази за одними й тими самими даними. Така повторювана робота марнує ресурси сервера та збільшує час очікування.
Кешування розв'язує цю проблему, зберігаючи дані, до яких часто звертаються, у швидкому шарі доступу. Хоча Redis та Memcached є популярними рішеннями, існує альтернатива, яку часто ігнорують: сам MongoDB. Якщо ви вже використовуєте MongoDB як основну базу даних, навіщо перевантажувати інфраструктуру ще одним сервісом?
Завдяки офіційному пакету mongodb/laravel-mongodb (версія 5.5.0 станом на 2025 рік), ви можете використовувати MongoDB як сховище кешу з нативною підтримкою TTL-індексів, які автоматично видаляють застарілі записи. Це спрощує вашу інфраструктуру, забезпечуючи при цьому відмінну продуктивність кешування.
# Що знадобиться
Перед початком переконайтеся, що у вас є:
• PHP 8.1 або вище зі встановленим розширенням MongoDB PHP.
• Laravel 11.x або 12.x — остання версія інтеграції Laravel з MongoDB підтримує обидві версії.
• MongoDB 4.4+, запущений локально, або кластер MongoDB Atlas.
• Composer для керування залежностями.
Якщо ви використовуєте Laravel Herd або встановлювали PHP через php.new, розширення MongoDB вже доступне. Перевірити його наявність можна командою:
php –ri mongodb
Ви повинні побачити версію розширення MongoDB та параметри конфігурації.
# Налаштування середовища
Спершу переконайтеся, що розширення MongoDB правильно встановлене та активоване як для CLI, так і для вебсервера:
php –ri mongodb
Якщо команда не повертає інформацію про розширення, його потрібно встановити:
pecl install mongodb
Після цього додайте рядок extension=mongodb.so (Linux/Mac) або extension=mongodb.dll (Windows) у ваш файл php.ini.
# Встановлення пакету MongoDB для Laravel
Пакет mongodb/laravel-mongodb офіційно підтримується командою MongoDB. Він містить драйвер кешу, підтримку Eloquent та розширення конструктора запитів.
Встановіть його за допомогою Composer:
composer require mongodb/laravel-mongodb
Підтримка Laravel 12 була додана у версії 5.2.0 у березні 2025 року. Пакет автоматично реєструє сервіс-провайдер, тому в Laravel 11+ ручне налаштування не потрібне.
# Налаштування MongoDB як драйвера кешу
Щоб увімкнути кешування через MongoDB, потрібно налаштувати два елементи: з'єднання з базою даних та сховище кешу.
# Налаштування з'єднання з базою даних
Відкрийте config/database.php і додайте з'єднання MongoDB:
'connections' => [
// ... існуючі з'єднання
'mongodb' => [
'driver' => 'mongodb',
'dsn' => env('MONGODB_URI', 'mongodb://localhost:27017'),
'database' => env('MONGODB_DATABASE', 'laravel'),
],
],
Потім оновіть файл .env актуальними даними:
MONGODB_URI=mongodb://localhost:27017
MONGODB_DATABASE=laravel
Для MongoDB Atlas рядок підключення виглядатиме інакше (відповідно до вашого кластера).
# Конфігурація сховища кешу
У файлі config/cache.php додайте налаштування для сховища mongodb:
'stores' => [
// ... існуючі сховища
'mongodb' => [
'driver' => 'mongodb',
'connection' => 'mongodb',
'collection' => 'cache',
'lock_connection' => 'mongodb',
'lock_collection' => 'cache_locks',
],
],
Зробіть MongoDB драйвером кешу за замовчуванням у файлі .env:
CACHE_STORE=mongodb
Або змініть значення безпосередньо у config/cache.php:
'default' => env('CACHE_STORE', 'mongodb'),
# Налаштування TTL-індексів
Індекси Time to Live (TTL) дозволяють MongoDB автоматично видаляти документи. Ми наполегливо рекомендуємо їх використовувати, оскільки вони:
- Автоматично видаляють застарілі записи — не потрібно писати додатковий код.
- Покращують продуктивність — MongoDB очищує дані у фоновому режимі.
- Економлять місце — колекція кешу залишається компактною.
Фоновий процес MongoDB перевіряє застарілі документи кожні 60 секунд і видаляє їх пачками до 50,000 за один раз.
Важливі вимоги до TTL-індексів:
- Вони працюють лише з однополевими індексами.
- Індексоване поле має містити дату або масив дат.
- MongoDB зберігає дати як типи BSON date, а не як рядки.
Створіть міграцію для налаштування TTL-індексів для кешу та блокувань:
php artisan make:migration create_mongodb_cache_indexes
Відкрийте створений файл і додайте наступний код:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Cache;
use MongoDB\Laravel\Cache\MongoStore;
use MongoDB\Laravel\Cache\MongoLock;
return new class extends Migration
{
public function up(): void
{
// Створення TTL-індексу для сховища кешу
$store = Cache::store('mongodb')->getStore();
if ($store instanceof MongoStore) {
$store->createTTLIndex();
}
// Створення TTL-індексу для блокувань кешу
$lock = Cache::store('mongodb')->lock('setup_lock');
if ($lock instanceof MongoLock) {
$lock->createTTLIndex();
}
}
public function down(): void
{
// TTL-індекси можна видалити вручну за потреби:
// db.cache.dropIndex("expires_at_1")
// db.cache_locks.dropIndex("expires_at_1")
}
};
Запустіть міграцію:
php artisan migrate
Метод createTTLIndex() створює індекс на полі expires_at, що дає команду MongoDB автоматично видаляти документи після завершення терміну їхньої дії.
# Базове використання кешу
Facade Cache в Laravel забезпечує єдиний API незалежно від обраного драйвера. Ось основні операції:
# Зберігання та отримання даних
Збереження значення в кеші з часом дії у секундах:
use Illuminate\Support\Facades\Cache;
// Зберігаємо на 60 секунд
Cache::put('user_count', 1500, 60);
// Зберігаємо на 1 годину за допомогою Carbon
Cache::put('dashboard_stats', $stats, now()->addHour());
// Отримуємо значення
$count = Cache::get('user_count');
// Отримуємо зі значенням за замовчуванням
$count = Cache::get('user_count', 0);
// Перевіряємо наявність ключа
if (Cache::has('user_count')) {
// Ключ існує і не застарів
}
// Видаляємо конкретний елемент
Cache::forget('user_count');
// Повне очищення кешу
Cache::flush();
# Патерн Remember
Метод remember намагається отримати дані з кешу, а якщо їх там немає — виконує функцію, зберігає результат і повертає його:
$users = Cache::remember('active_users', 3600, function () {
return User::where('status', 'active')
->with('profile')
->get();
});
Це ідеально підходить для важких запитів до бази даних. Перший запуск виконає запит, а наступні звернення протягом години повертатимуть дані миттєво.
Для даних, що майже не змінюються, використовуйте rememberForever:
$settings = Cache::rememberForever('app_settings', function () {
return Setting::all()->pluck('value', 'key')->toArray();
});
# Інкремент та декремент
Кеш MongoDB підтримує атомарні операції з числами:
// Ініціалізація лічильника
Cache::put('page_views', 0, 3600);
// Збільшення на 1
Cache::increment('page_views');
// Збільшення на певне значення
Cache::increment('page_views', 10);
// Зменшення
Cache::decrement('api_calls_remaining');
Cache::decrement('api_calls_remaining', 5);
// Працює навіть із числами з рухомою комою (float)
Cache::increment('total_revenue', 99.99);
Ці операції атомарні, тому їх безпечно використовувати у високонавантажених системах, де кілька процесів можуть оновлювати лічильник одночасно.
# Практичний приклад: Каталог товарів
Використаємо кешування у ProductController для прискорення роботи каталогу:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Support\Facades\Cache;
class ProductController extends Controller
{
public function index() {
// Кешуємо каталог на 30 хвилин
$products = Cache::remember('products:catalog', 1800, function () {
return Product::with(['category', 'images'])
->where('status', 'active')
->orderBy('featured', 'desc')
->orderBy('created_at', 'desc')
->limit(100)
->get();
});
return view('products.index', compact('products'));
}
public function show(string $slug) {
// Кешуємо окремий товар на 1 годину
$product = Cache::remember("product:{$slug}", 3600, function () use ($slug) {
return Product::with(['category', 'images', 'variants', 'reviews'])
->where('slug', $slug)
->firstOrFail();
});
return view('products.show', compact('product'));
}
public function update(Request $request, string $id) {
$product = Product::findOrFail($id);
$product->update($request->validated());
// Інвалідація кешу після оновлення
Cache::forget("product:{$product->slug}");
Cache::forget('products:catalog');
return redirect()->back()->with('success', 'Товар оновлено');
}
}
Цей контролер кешує і список товарів, і сторінки окремих продуктів. При оновленні товару ми очищуємо відповідні ключі, щоб користувачі бачили актуальну інформацію.
# Обмеження кешування в MongoDB
Варто знати про одне важливе обмеження: драйвер кешу MongoDB не підтримує теги кешу (Cache tags).
Теги працюють із драйверами, що зберігають дані в оперативній пам'яті (Redis, Memcached), але не з базами даних на кшталт MongoDB чи DynamoDB. Це зумовлено специфікою структур даних.
Якщо вам потрібна функціональність тегів, є два виходи:
Варіант 1: Використовуйте префікси ключів для організації записів:
// Групуємо через префікси замість тегів
Cache::put('products:featured:list', $products, 3600);
Cache::put('products:category:electronics', $electronics, 3600);
// Видаляємо за списком ключів
Cache::forget('products:featured:list');
Cache::forget('products:category:electronics');
Варіант 2: Використовуйте Redis для тегів паралельно з MongoDB:
// У config/cache.php додайте обидва сховища
// MongoDB для загального кешу
Cache::store('mongodb')->put('user_profile', $profile, 3600);
// Redis, коли потрібні теги
Cache::store('redis')->tags(['products', 'featured'])
->put('featured_products', $products, 3600);
Для більшості проєктів підхід із префіксами є цілком достатнім і дозволяє не ускладнювати інфраструктуру.
# Розподілені блокування (Distributed locks)
Щоб уникнути конфліктів, коли кілька процесів одночасно намагаються оновити ті самі дані, використовуйте блокування:
use Illuminate\Support\Facades\Cache;
public function processOrder(string $orderId)
{
// Намагаємося отримати блокування на 10 секунд
$lock = Cache::lock("order:processing:{$orderId}", 10);
if ($lock->get()) {
try {
// Обробка замовлення...
$this->processPayment($orderId);
} finally {
$lock->release();
}
} else {
throw new \Exception('Замовлення вже обробляється іншим процесом');
}
}
Для очікування вивільнення блокування використовуйте метод block:
// Чекаємо до 5 секунд
$lock = Cache::lock('inventory:update', 10);
$lock->block(5, function () {
// Код виконається, коли блокування буде отримано
$this->updateInventory();
});
MongoDB зберігає блокування у колекції cache_locks. Завдяки TTL-індексам вони автоматично зникають, якщо процес раптово завершився помилкою.
# MongoDB проти Redis для кешування
Вибір залежить від потреб вашого застосунку:
| Фактор | MongoDB | Redis |
|---|---|---|
| Інфраструктура | Використовує існуючий екземпляр | Потребує окремого сервера |
| Продуктивність | Відмінна для більшості задач | Трохи швидша (субмілісекунди) |
| Масштабованість | Краще горизонтальне масштабування | Обмежена обсягом RAM |
| Гнучкість запитів | Повноцінна мова запитів MongoDB | Лише пошук за ключем |
| Надійність | Повна персистентність за замовчуванням | Можлива втрата даних при збої |
Обирайте MongoDB, якщо:
- Це ваша основна база даних.
- Ви хочете мінімізувати кількість сервісів у стеку.
- Потрібне надійне зберігання кешованих даних.
Обирайте Redis, якщо:
- Потрібна мінімально можлива затримка (latency).
- Ви використовуєте складні структури (sorted sets, pub/sub).
- Ви будуєте real-time сервіси (чати, лідерборди).
# Кращі практики
# Оптимізація індексів
Для швидкого пошуку створіть індекс на полі key:
$collection = DB::connection('mongodb')->getCollection('cache');
$collection->createIndex(['key' => 1]);
Це суттєво прискорить роботу, коли обсяг кешу почне зростати.
# Прогрівання кешу (Cache warming)
Замість того, щоб чекати, поки перший користувач "спровокує" створення кешу, наповнюйте його заздалегідь під час деплою:
// Створіть консольну команду Artisan
public function handle()
{
$this->info('Прогріваємо кеш...');
Cache::put('products:featured', Product::featured()->limit(20)->get(), 3600);
$this->info('Готово!');
}
Використовуйте php artisan cache:warm у скриптах автоматичного деплою.
# Вирішення типових проблем
Проблема: Помилка дубліката ключа в cache_locks.
Рішення: Це нормальна поведінка MongoDB для забезпечення унікальності блокування. Переконайтеся, що пакет mongodb/laravel-mongodb оновлено до версії 5.2.0+, де ця ситуація коректно обробляється.
Проблема: TTL-індекс не видаляє записи.
Рішення: Перевірте наявність індексу через MongoDB shell (db.cache.getIndexes()). Пам'ятайте, що видалення відбувається не миттєво, а протягом хвилини.
Проблема: Помилки серіалізації Eloquent-моделей.
Рішення: Замість цілих моделей кешуйте масиви: Cache::put('product', $product->toArray(), 3600). Це надійніше і займає менше місця.
# Висновок
Кешування через MongoDB — це практичне рішення для Laravel-проєктів, які вже використовують цю базу даних. Ви отримуєте потужний інструмент з автоматичним керуванням TTL без необхідності підключати Redis.
Почніть із кешування найважчих запитів, стежте за показниками Hit Rate у Laravel Pulse і використовуйте прогрівання кешу під час деплою. Це дозволить значно прискорити ваш застосунок, зберігаючи архітектуру простою та зрозумілою.