Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Senior: питання на співбесіді з теми «Кешування»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

Cache Stampede (dogpile) - коли популярний ключ кешу протермінувався, і сотні одночасних запитів кидаються перераховувати важке значення одночасно, перевантажуючи БД.

Рішення в Laravel:

// блокування: лише один процес перераховує, інші чекають результат
$value = Cache::lock('report:lock', 10)->block(5, function () {
    return Cache::remember('report', 3600, fn () => $this->heavyReport());
});

Інші стратегії:

  • Cache::flexible() (stale-while-revalidate) - віддає «протухле» значення, поки одне фонове оновлення його перераховує.
  • Розмазування TTL (jitter), щоб ключі не протухали одночасно.
  • Прогрів кешу (cache warming) за розкладом, а не за запитом користувача.

Докладніше в документації: Кеш: атомарні блокування

Cache::lock() дає розподілене блокування: лише один процес на всіх серверах виконує ділянку коду одночасно.

$lock = Cache::lock("invoice:{$order->id}:generate", 30);

if ($lock->get()) {
    try {
        $order->generateInvoice();
    } finally {
        $lock->release();
    }
}

Або коротше - з очікуванням до 5 секунд:

Cache::lock("invoice:{$order->id}:generate", 30)->block(5, function () use ($order) {
    $order->generateInvoice();
});

Де це потрібно:

  • подвійний клік чи повторний вебхук не має створити два рахунки;
  • запланована команда, що запускається на кількох серверах;
  • перерахунок дорогого кешу - лише один процес рахує, інші чекають.

Важливі деталі:

  • Час життя блокування (30 с) - страховка, якщо процес упав і не звільнив його. Якщо робота може тривати довше, блокування закінчиться посередині, і зайде другий процес. Його подовжують через refresh().
  • Власник: release() звільняє лише своє блокування. Щоб звільнити з іншого процесу (наприклад, у завданні черги), передають токен через owner() і restoreLock().
  • Драйвер: потрібен спільний для всіх серверів - Redis, Memcached, database, DynamoDB. file блокує лише в межах одного сервера.

Блокування не замінює унікальних індексів у базі: індекс - остання лінія захисту, блокування лише зменшує кількість конфліктів.

Докладніше в документації: Атомарні блокування

Модель у кеші серіалізується цілком - з атрибутами, завантаженими зв'язками й службовим станом. Звідси кілька проблем.

1. Безпека десеріалізації. unserialize() даних, які можна підробити, - класичний шлях до виконання коду. У Laravel 13 конфігурація кешу має опцію serializable_classes, і за замовчуванням об'єкти з кешу не відновлюються. Щоб кешувати моделі, їхні класи (і класи вкладених колекцій) доводиться явно дозволити.

2. Застарілі дані. Закешована модель із завантаженими зв'язками - знімок усього дерева. Змінили автора - а в кеші поста він старий, і скинути треба вже не один ключ.

3. Зміна коду. Після деплою з новим атрибутом чи кастом старі серіалізовані об'єкти можуть відновитися в неконсистентному стані.

4. Розмір. Серіалізована модель зі зв'язками займає в рази більше пам'яті Redis, ніж потрібні поля.

Що кешують замість цього:

// ідентифікатори - а моделі дістають швидким запитом за ключем
$ids = Cache::remember('posts:popular', 600, fn () => Post::popular()->limit(10)->pluck('id')->all());
$posts = Post::with('author')->findMany($ids);

// або готові масиви для відображення
$menu = Cache::remember('menu', 3600, fn () => Category::query()->get(['name', 'slug'])->toArray());

Правило: кешують результат дорогого обчислення в найпростішій формі - скаляри, масиви, ID. Дорогим зазвичай є пошук і агрегація, а не завантаження рядка за первинним ключем.

Докладніше в документації: Збереження елементів у кеші