Головна / Статті

Основи кешування в Redis: шаблони, поширені помилки та концепції для співбесід.

Кешування в Redis у Node.js: метод cache-aside, TTL, захист від надмірного навантаження та політики видалення даних, а також запитання до співбесіди з проектування систем, пов’язані з цими аспектами.

Видавець: REACTAPP.TOP11 хв читання2516 слів

Джерела: Redis Isn’t Just a Cache: The Concepts Every Backend Developer Should Know

Чому нам потрібна кеш-пам’ять, якщо у нас вже є база даних?

GET /products/123

Клієнт звертається до цього маршруту, і ваш сервер починає запитувати MongoDB:

API → MongoDB → Product

База даних повертає запис про продукт, і все працює без проблем.

10 000 людей, які запитують про той самий продукт приблизно в один і той самий час.

У результаті доведеться тисячі разів повторювати абсолютно однакові операції:

Request
   ↓
Database
   ↓
Same Product

Ваша база даних постійно виконує ідентичну роботу для однакових запитів.

кешування.

Що таке кешування?

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

Замість звичайного алгоритму:

Request → Database → Response

ми пропонуємо скорочений шлях:

Request
   ↓
Cache
   ↓
Hit? ── Yes → Response
   │
   No
   ↓
Database
   ↓
Cache
   ↓
Response

Мета не в тому, щоб повністю позбутися бази даних.

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

Успішний пошук у кеші vs Неуспішний пошук у кеші

Ці два поняття зустрічаються постійно, тож варто знати їх наизусть.

Успішний пошук у кеші

Це трапляється, коли дані, які ви шукаєте, вже знаходяться у кеші.

Request
  ↓
Redis
  ↓
Found ✅
  ↓
Response

Неуспішний пошук у кеші

Це трапляється, коли у кеші немає потрібних даних.

Request
  ↓
Redis
  ↓
Not Found ❌
  ↓
Database
  ↓
Store in Redis
  ↓
Response

Корисним показником тут є коефіцієнт успішних пошуків у кеші:

Cache Hit Ratio =
Cache Hits / Total Cache Requests

Вищий коефіцієнт зазвичай свідчить про те, що більше запитів обробляється безпосередньо з кешу, а не з бази даних.

Проте вище число не означає автоматично успіху. Якщо кеш заповнений застарілими чи менш важливими даними, високий коефіцієнт успішних запитів не робить його корисним.

То яке місце тут займає Redis?

Redis — це система зберігання даних у оперативній пам’яті, яка часто використовується для кешування, хоча її можливості значно перевищують цей один сценарій використання.

Вона пропонує низку структур даних, зокрема:

  • Рядки
  • Хеші
  • Списки
  • Множини
  • Впорядковані множини
  • Стріми
  • JSON
  • А також інші спеціалізовані типи

Для простого кешу можна уявити його ось так:

Key                 Value
────────────────────────────
product:123         Product data
user:42             User data

Оскільки Redis зберігає активні дані в пам’яті, операції читання з кешу можуть виконуватися дуже швидко.

Redis у додатку Node.js

Щоб використовувати клієнт node-redis, його встановлюють за допомогою:

npm install redis

Налаштування базового з’єднання вимагає лише кількох рядків коду:

import { createClient } from "redis";
const redis = createClient();
await redis.connect();

Після цього зберігання та отримання значень є простими:

await redis.set("user:42", "Hamza");
const user = await redis.get("user:42");

Офіційна документація Redis наразі рекомендує node-redis як клієнта для проектів Node.js.

Однак знання команд SET та GET допомагають лише частково.

Справжнє питання, на яке варто відповісти, звучить так:

Як насправді проектувати додаток з використанням Redis?

Найважливіший паттерн: Cache-Aside

Якщо існує один шаблон, який кожен розробник бекенду повинен спочатку опанувати, то це Cache-Aside.

У цьому шаблоні сама програма відповідає за керування кешем.

Request
   ↓
Check Redis
   ↓
 ┌───────────────┐
 │ Cache Hit?    │
 └───────────────┘
    ↓ Yes    ↓ No
 Response   Database
              ↓
           Redis
              ↓
           Response

Спрощена версія в коді виглядає так:

const cached = await redis.get(key);
if (cached) return JSON.parse(cached);const product = await Product.findById(id);await redis.set(key, JSON.stringify(product), {
    EX: 300
});

Параметр EX: 300 встановлює TTL у 300 секунд для запису в кеші.

Cache-Aside, мабуть, є найпоширенішим шаблоном кешування з Redis.

TTL: Кеш не повинен існувати вічно

Припустимо, ви зберегли у кеші наступне:

product:123

Що має статися, якщо ціну товару пізніше оновити?

Без терміну закінчення дії користувачі могли б безкінечно бачити стару ціну.

Ось чому існує поняття TTL, або Time To Live.

Product stored
     ↓
TTL = 300 seconds
     ↓
Expires
     ↓
Next request → Database → Fresh cache

TTL встановлює обмеження на те, як довго можуть надаватися застарілі дані.

Проте утримуйтеся від бажання обрати надто довгий TTL.

Правильне значення залежить виключно від того, наскільки застарілі дані може допустити ваша програма.

Анулювання кешу: відома складна проблема

Розгляньмо таку ситуацію:

Database:
price = ₹799
Redis:
price = ₹799

Потім база даних оновлюється:

Database:
price = ₹899

Але Redis все ще зберігає:

price = ₹799

На цьому етапі ваша програма надає несумісні дані залежно від того, звідки вона їх читає.

Поширеним рішенням є очищення кешу щоразу, коли змінюються базові дані:

await Product.findByIdAndUpdate(id, update);
await redis.del(`product:${id}`);

Після цього наступний запит проходить цим шляхом:

Redis → Miss
   ↓
Database → ₹899
   ↓
Redis → Store fresh value

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

Три проблеми кешу, про які варто знати

Це чудові теми для обговорень під час співбесід.

1. Cache Stampede

product:123 → EXPIRED

Відразу після цього надходить 1 000 запитів одночасно.

Кожен з них не знаходить інформації в кешу.

Кожен з них передається до бази даних.

1000 requests
      ↓
1000 DB queries 💥

Кеш мав захищати базу даних від надмірного навантаження, але сама подія закінчення терміну дії лише спричинила стрибок навантаження.

Типові способи захисту від цього включають:

  • Блокування або послідовні запити, щоб лише один запит оновлював кеш, поки інші чекають
  • Різні таймери закінчення життєвого циклу, щоб ключі не закінчували термін дії одночасно
  • Ймовірнісне передчасне оновлення, яке продовжує термін дії ключа трохи перед його фактичним закінченням
  • Заздалегідь попереднє нагрівання часто запитуваних даних
  • У власній документації Redis прямо описано стратегії захисту від надмірних запитів до «гарячих» ключів.

    2. Проникнення в кеш

    Що відбувається, коли клієнти постійно запитують дані, яких просто не існує?

    GET /users/999999
    

    Redis повертає результат «не знайдено».

    База даних повертає повідомлення «не знайдено».

    Наступний ідентичний запит повторює точно такий самий цикл.

    Зловмисник або несправний клієнт може скористатися цим, щоб завантажувати базу даних марними запитами.

    Корисним заходом протидії є негативний кешування — кешування інформації про відсутність чогось:

    user:999999 → NOT_FOUND
    TTL → short
    

    Завдяки цьому повторні пошуки того самого відсутнього запису можна виконувати безпосередньо у Redis, замість того щоб щоразу звертатися до бази даних.

    3. Аваланш кешу

    Тепер уявіть собі велику кількість ключів у кеші, які закінчують свою дію приблизно в один і той самий момент.

    Key A → expired
    Key B → expired
    Key C → expired
    Key D → expired
           ↓
    Massive DB traffic
    

    Цю ситуацію називають аваланшем кешу.

    Типовим способом зменшення ризику є уникнення призначення однакових термінів закінчення дії для великих груп ключів — наприклад, шляхом додавання невеликої випадкової похибки до кожного TTL.

    Видалення з кешу: що відбувається, коли у Redis закінчується пам’ять?

    Пам’ять Redis не є нескінченною.

    Як тільки використання пам’яті досягає налаштованого ліміту, Redis починає видаляти ключі згідно з правилом видалення, яке ви налаштували. У Redis є кілька таких режимів видалення, які дозволяють видаляти найрідше використовувані записи у всьому просторі ключів або лише серед ключів із встановленим терміном дії, видаляти найрідше використовувані записи у будь-якому з цих двох діапазонів, видаляти записи залежно від того, які ключі найближче до закінчення терміну дії, або повністю блокувати нові записи, коли пам’ять заповнена.

    LRU по суті означає:

    Позбутися того, чим давно не користувалися.

    LFU по суті означає:

    Позбутися того, до чого дуже рідко звертаються.

    Вибір найкращого режиму залежить від вашої конкретної моделі доступу.

    Redis — це не лише кеш

    Це питання часто виникає під час інтерв’ю.

    Оскільки Redis пропонує кілька вбудованих структур даних, він може використовуватися не лише для кешування:

    Strings       → Caching
    Hashes        → Objects / sessions
    Lists         → Queues
    Sets          → Unique collections
    Sorted Sets   → Leaderboards
    Streams       → Event processing
    

    Отже, твердження, що:

    "Redis — це база даних для кешування"

    розповідає лише частину історії.

    Більш точний опис такий:

    Redis — це сховище структур даних у оперативній пам’яті, яке зазвичай використовується для кешування, але також здатне обробляти сесії, черги, топ-листи, обмеження швидкості, системи повідомлень типу pub/sub та багато іншого.

    Офіційна документація Redis щодо типів даних розглядає ці структури разом із іншими спеціалізованими типами, які він підтримує.

    А як щодо стабільності даних у Redis?

    Оскільки Redis знаходиться в оперативній пам’яті, чи призводить перезапуск до втрати всього?

    Не обов’язково.

    Redis пропонує кілька стратегій збереження даних:

    • RDB — періодичні знімки даних у певний момент часу
    • AOF — журнал, який фіксує кожну операцію запису
    • RDB + AOF — поєднання обох методів
    • Без збереження даних взагалі

    Якщо Redis використовується лише як кеш, втрата його вмісту після перезавантаження часто не має значення, оскільки додаток може просто відновити дані з основної бази даних.

    Але якщо Redis зберігає важливі дані самостійно, налаштування збереження стають значно важливішими.

    Поширена пастка на співбесіді

    «Чому б просто не зберігати все в Redis?»

    Тому що пам’ять Redis — це обмежений та відносно дорогий ресурс порівняно з традиційними методами постійного збереження.

    Це означає, що не варто бездумно використовувати кешування.

    Хороші кандидати на кешування мають такі характеристики:

    • Їх часто запитують
    • Їх отримання чи обчислення є дорогим
    • Певний старіння даних є прийнятним
    • Вони варті витрат пам’яті на їх зберігання поблизу

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

    Локальний кеш проти розподіленого кешу

    Server A → Cache A
    Server B → Cache B
    Server C → Cache C
    

    Кожен з цих серверів у кінцевому підсумку підтримує власний незалежний кеш.

    Користувач може потрапити на сервер A та отримати дані з кешу, а потім — на сервер B та не знайти ці самі дані в кешу.

    Введення спільної інстанції Redis змінює ситуацію:

                 ┌→ Server A ─┐
    Client ──────┼→ Server B ─┼→ Redis
                 └→ Server C ─┘
    

    Тепер кожна інстанція додатку користується одним і тим самим спільним кешем.

    Ця перевага спільного стану є важливою причиною того, чому Redis особливо ефективний під час горизонтального масштабування додатку.

    Деталі інтерв’ю

    Припустимо, співрозмовник під час інтерв’ю поставить таке завдання:

    "Розробіть стратегію кешування для API продукту з високим трафіком."

    Слабка відповідь обмежується таким:

    "Я використаю Redis."

    Краща відповідь містить обґрунтування:

    Request
                         ↓
                      Redis
                    ↙       ↘
                 Hit         Miss
                  ↓            ↓
               Response      Database
                                ↓
                             Redis
                                ↓
                             Response
    

    Далі описуються кожні з цих рішень щодо проектування:

    1. Шаблон кешування

    Використовуємо підхід cache-aside.

    2. TTL

    Встановлюємо його залежно від того, наскільки довго дані продукту можуть залишатися застарілими.

    3. Анулювання даних

    Видаляємо відповідний ключ щоразу, коли оновлюються базові дані.

    4. Захист від надмірного навантаження

    Використовуйте техніки блокування, об’єднання запитів чи передчасного оновлення даних.

    5. Видалення елементів

    Виберіть відповідну політику LRU чи LFU для конкретного навантаження.

    6. Проектування ключів

    product:{id}
    user:{id}
    

    7. Моніторинг

    Стежте за:

    • Коефіцієнтом успішних запитів
    • Коефіцієнтом невдалих запитів
    • Затримкою виконання
    • Використанням пам’яті
    • Кількістю видалених елементів
    • Популярними ключами
    • Помилками Redis

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

    Швидкий огляд інтерв’ю

    Навіщо взагалі використовувати кешування?

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

    Чому саме Redis?

    Він забезпечує швидкий доступ відразу з пам’яті, підтримку терміну дії, універсальні структури даних та функції, розроблені для розподілених систем.

    Що означає «cache-aside»?

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

    Що означає TTL?

    Це термін існування запису в кеші, після якого він автоматично закінчує свою дію.

    Що означає анулювання кешу?

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

    Що таке «cache stampede»?

    Це ситуація, коли велика кількість запитів одночасно не знаходить даних у кеші та починає активно запитувати базу даних.

    LRU проти LFU?

    LRU видаляє ті дані, які використовувалися останньо. LFU видаляє ті дані, які використовувалися найрідше загалом.

    Чи кожен запит заслуговує на кешування?

    Зовсім ні. Ухвалюйте рішення, ґрунтуючись на тому, як часто доступуються дані, наскільки дорогим є їх отримання, наскільки допустимою є старість даних та скільки пам’яті у вас є вільною.

    Модель мислення, яку варто зберегти

    Не намагайтеся запам’ятати команди Redis як початкову точку. Натомість зосередьтесь на основній архітектурі:

                  ┌─────────────┐
    Request ─────→│    Redis    │
                  └──────┬──────┘
                         │
                  Cache Hit?
                   ↙         ↘
                 YES           NO
                  ↓             ↓
              Response       Database
                                ↓
                             Redis
                                ↓
                             Response
    

    Потім розгляньте чотири запитання:

    Що має знаходитися в кеші?

    Як довго воно має там залишатися?

    Коли його потрібно анулювати?

    Що станеться, якщо кеш зламається або закінчить термін дії під великим навантаженням?

    Розгляд цих запитань — ось що відрізняє поверхневі знання про кешування від мислення рівня продакшну.

    Підсумок

    Кешування — це не просто:

    "Розмістіть Redis перед MongoDB."

    Якісна стратегія кешування ґрунтується переважно на компромісах.

    Ви змінюєте:

    Пам’ять на швидкість

    а іноді:

    Актуальність на продуктивність

    Redis надає механізми, але саме логіка вашого додатку визначає, як ними скористатися.

    Якщо ви готуєтесь до співбесід, не обмежуйтеся відповіддю:

    "Redis — це база даних у пам’яті.“

    Будьте готові детально розповісти про:

    модель кешування типу cache-aside, TTL, анулювання даних, коефіцієнт успішних запитів, видалення даних з кешу, ефекти масового доступу, проникнення, лавинний ефект, розподілене кешування та послідовність даних.

    Як тільки ви зрозумієте, як ці концепції взаємопов’язані, ви вийдете за межі простого знання Redis.

    Тепер ви розумієте, як системи з високим трафіком захищають свої бази даних, зберігаючи при цьому швидкість.