Middle: питання на співбесіді з теми «Кешування»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
3 питання
Єдиний API поверх драйверів для зберігання результатів важких обчислень чи запитів. Драйвери: database (за замовчуванням у нових застосунках), file, redis, memcached, dynamodb, array (для тестів). Задається через CACHE_STORE у .env.
remember - найпоширеніший патерн (дістати з кешу або обчислити й закешувати):
$users = Cache::remember('active_users', 3600, function () {
return User::where('active', true)->get();
});
Cache::put('key', $value, now()->addMinutes(10));
$value = Cache::get('key', 'default');
Cache::forget('key');
Теговане кешування (лише Redis/Memcached) - для групового скидання:
Cache::tags(['posts'])->put('post.1', $post, 600);
Cache::tags(['posts'])->flush(); // скинути всю групу
Атомарні блокування проти гонок (один процес у критичній секції):
Cache::lock('processing', 10)->get(function () {
// критична секція
});
Найскладніше - інвалідація: кеш скидають у подіях/обзерверах моделей при зміні даних. Застарілий кеш часто гірший за його відсутність.
Найскладніше в кешуванні - не покласти значення, а вчасно його прибрати. Є три основні підходи.
1. Явне видалення при зміні:
class Category extends Model
{
protected static function booted(): void
{
static::saved(fn () => Cache::forget('categories:menu'));
static::deleted(fn () => Cache::forget('categories:menu'));
}
}
Просто, але треба пам'ятати всі ключі, які залежать від моделі. Масові оновлення Category::query()->update() подій моделі не викликають - кеш лишиться старим.
2. Теги - скинути групу ключів однією командою:
Cache::tags(['posts', 'user:42'])->remember('user:42:posts', 3600, fn () => /* ... */);
Cache::tags('posts')->flush(); // усе, що стосується постів
Працює лише з драйверами, що підтримують теги (Redis, Memcached), - не з file і database.
3. Версія в ключі - нічого не видаляти, а змінювати ключ:
$version = Cache::rememberForever('posts:version', fn () => 1);
$posts = Cache::remember("posts:v{$version}:page:{$page}", 3600, fn () => /* ... */);
// при зміні
Cache::increment('posts:version');
Старі ключі просто перестають читатися й самі зникають за TTL. Працює з будь-яким драйвером.
Правило: короткий TTL - страховка на випадок, якщо скидання десь забули. Довгий TTL без надійного скидання - гарантовані скарги на застарілі дані.
Cache::flexible() реалізує підхід «stale-while-revalidate»: коли значення застаріло, користувач одразу отримує старе, а нове рахується у фоні.
$stats = Cache::flexible('dashboard:stats', [300, 900], function () {
return DashboardStats::calculate(); // повільний розрахунок
});
Масив - два пороги в секундах:
- до 300 - значення свіже, віддається як є;
- від 300 до 900 - застаріле, але прийнятне: віддається одразу, а перерахунок запускається після відправлення відповіді;
- після 900 - надто старе, рахується синхронно, як у
remember().
Проблема remember(), яку це вирішує: коли TTL закінчився, саме той користувач, який прийшов першим, чекає на весь повільний розрахунок. На популярній сторінці кілька таких запитів одночасно ще й навантажать базу (cache stampede).
Коли підходить:
- дані, де хвилина затримки не має значення: статистика, рейтинги, лічильники, зовнішні API;
- розрахунок довгий, а сторінку відкривають часто.
Коли ні: ціни в кошику, залишки на складі, права доступу - там застаріле значення означає помилку, а не трохи старіші цифри.
Перерахунок у фоні виконується через відкладені функції (defer) після відправлення відповіді, тож окрема черга для цього не потрібна.