Модуль 28

Проблеми кешування та cache invalidation

Ось твій урок у стилі CS50. Вмикаймо прожектори, беремо мікрофон — поїхали! 🚀


🎓 CS50: Проблеми кешування та Cache Invalidation

(David Malan style)


1. 🔥 Вступ: Чому мій холодильник бреше?

Уявіть, що ви — студент у гуртожитку. Ви страшенно хочете їсти. У вас є два варіанти: 1. Піти в магазин (це далеко, довго, треба одягатися). 2. Відкрити холодильник (це миттєво, рукою подати).

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

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

Це і є проблема кешування.

У програмуванні ми постійно стикаємося з цим. * Ви змінили аватарку в Instagram, а друзі бачать стару. Чому? * Ви змінили ціну товару в адмінці, а на сайті вона досі стара. Чому?

Без кешування сучасний інтернет впав би за 5 хвилин. Бази даних не витримали б навантаження, якби ми за кожним лайком ходили "в магазин" (на сервер БД). Але з кешуванням приходить одна з найскладніших проблем в Computer Science.

Питання до вас: Чи траплялося вам натискати F5 (оновити сторінку) кілька разів, бо сайт показував щось старе? Вітаю, ви стали жертвою "протухлого" кешу.


2. 🧠 Теоретична база: Як це працює «під капотом»

Давайте розберемо це без складних формул.

У нас є Джерело Правди (Database / API) — це "магазин". Там завжди все свіже і правильне, але доступ туди повільний. У нас є Кеш (Redis, Memcached, пам'ять браузера) — це "холодильник". Швидкий, але дані там можуть застаріти.

Два головні поняття:

  1. Cache Hit (Влучання в кеш): Ви відкрили холодильник, піца там є. Супер! Ми зекономили час.
  2. Cache Miss (Промах повз кеш): Холодильник порожній. Доводиться йти в магазин (робити важкий запит до БД), купувати піцу і класти її в холодильник для наступного разу.

А тепер — монстр 👹: Cache Invalidation

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

Чому це складно? Уявіть, що ви змінили своє ім'я у паспортному столі (База Даних). Але у вашого вахтера, у відділі кадрів і у бібліотеці (різні шари кешу) записане старе прізвище. Як сповістити їх усіх одночасно, що дані змінилися?

Що треба запам'ятати залізобетоном:

Кеш — це завжди копія. А копія має властивість застарівати. Ваша задача — знати, коли її оновити.

Інтуїтивно зрозумійте: Є два основні підходи боротьби зі старими даними: 1. TTL (Time To Live): Таймер. "Ця піца лежить тут 2 години, потім викидаємо, незалежно від того, зіпсувалася вона чи ні". 2. Event-based (Подієва): "Сусід з’їв піцу — викинь коробку негайно".


3. 🧪 Приклади: Від простого до болючого

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

Приклад 1: Ідеальний світ (Get)

У нас є функція, яка дістає профіль користувача. Це "важка" операція (йде в базу).

# База даних (повільна)
database = {
    "user:1": "David Malan"
}

# Кеш (швидкий)
cache = {}

def get_user(user_id):
    # 1. Спочатку питаємо кеш
    if user_id in cache:
        print("⚡️ Знайшли в кеші!")
        return cache[user_id]

    # 2. Якщо немає - йдемо в базу (імітуємо затримку)
    print("🐢 Йдемо в базу даних...")
    user = database.get(user_id)

    # 3. Зберігаємо в кеш на майбутнє
    cache[user_id] = user
    return user

Питання до вас: Що виведе програма, якщо я викличу get_user("user:1") два рази поспіль?

Очікування: Перший раз — "🐢 Йдемо в базу...", другий раз — "⚡️ Знайшли в кеші!". Це працює ідеально.


Приклад 2: Проблема (Update)

А тепер уявіть, що David змінив ім'я на "David J. Malan".

def update_user_name(user_id, new_name):
    # Оновлюємо ТІЛЬКИ базу
    database[user_id] = new_name
    print(f"✅ У базі змінено на {new_name}")

# Сценарій:
get_user("user:1")           # Кешуємо "David Malan"
update_user_name("user:1", "David J. Malan") # Міняємо в базі
print(get_user("user:1"))    # ??? Що тут буде?

Пояснення: Ми отримаємо старе ім'я "David Malan". Чому? Бо ми оновили "магазин", але в нашому "холодильнику" (кеші) лежить старий продукт. Ми забули про інвалідацію!


Приклад 3: Вирішення (Invalidation)

Як це виправити? Досвідчений розробник знає: коли пишеш — чисти кеш.

def update_user_name_fixed(user_id, new_name):
    # 1. Оновлюємо базу
    database[user_id] = new_name

    # 2. 🔥 ІНВАЛІДАЦІЯ: Видаляємо старий запис з кешу
    if user_id in cache:
        del cache[user_id]
        print("🗑 Кеш очищено!")

# Сценарій:
get_user("user:1")                # Кешуємо старе
update_user_name_fixed("user:1", "SUPER DAVID") # Оновлюємо і чистимо
print(get_user("user:1"))         # Промах кешу -> Йдемо в базу -> Отримуємо "SUPER DAVID"

Бачите? Нам довелося один раз сходити в базу знову, але зате дані коректні.


4. 🛠 Практична частина

Час закачати рукави. Спробуйте вирішити ці завдання (можна в голові, можна на папірці, краще в IDE).

Завдання 1: TTL (Time To Live) Модифікуйте функцію get_user з Прикладу 1 так, щоб дані в кеші зберігалися разом з часом додавання. Якщо пройшло більше 5 секунд — кеш має вважатися недійсним, і ми знову йдемо в базу.

Завдання 2: "Ледача" інвалідація Ви пишете систему новин. Новини редагують рідко, читають часто. * Варіант А: Чистити кеш при кожному редагуванні статті. * Варіант Б: Просто поставити TTL 60 секунд. * Питання: Який варіант кращий для новинного сайту CNN/BBC і чому? Що станеться у Варіанті Б, якщо новина терміново змінилася (наприклад, помилка в заголовку)?

Завдання 3: Кейс "Лічильник лайків" У вас є пост в Instagram. Його лайкають 1000 разів на секунду. Якщо ви будете при кожному лайку робити UPDATE database і DELETE cache, ваша база "ляже". Подумайте: Як використовувати кеш, щоб збирати лайки і записувати їх у базу, скажімо, раз на 10 секунд? (Це називається Write-Back).

Завдання 4: Bug Hunting Розробник Вася написав код: 1. Оновити запис у БД. 2. Оновити запис у Кеші новими даними. Ситуація: Під час кроку 2 стався збій мережі (кеш не оновився). Наслідок: У базі нові дані, у кеші старі. Завдання: Як змінити логіку, щоб мінімізувати ризик розсинхронізації? (Підказка: чи краще оновлювати кеш чи видаляти його?)


5. 💡 Мислення як у розробника

Ось вам цитата легендарного Філа Карлтона, яку знає кожен сеньйор:

"У Computer Science є тільки дві складні речі: інвалідація кешу та придумування назв."

Чому він так сказав? Бо це джерело нескінченних багів.

Типові помилки новачків:

  1. Кешувати все підряд. Не треба кешувати те, що змінюється щосекунди (наприклад, точний час на сервері), або те, що запитують раз на рік.
  2. Забувати про інвалідацію. Ви змінили ціну товару, а клієнт купує за старою ціною. Бізнес втрачає гроші. Ви втрачаєте роботу.
  3. Race Conditions. Коли два процеси одночасно намагаються оновити кеш.

Як думає профі:

  • "Чи можу я дозволити собі показати користувачу старі дані на 5 хвилин?" (Якщо це кількість переглядів відео — так. Якщо це баланс банківського рахунку — НІКОЛИ).
  • Профі частіше видаляє ключ з кешу при зміні, ніж намагається його оновити. Видалити — надійніше. Наступний запит сам підтягне свіжі дані.

6. 🧩 Підсумок

Отже, що ми маємо сьогодні в сухому залишку:

  1. Кеш — це прискорювач. Це ваш холодильник, щоб не бігати в магазин.
  2. Stale Data (Протухлі дані) — головний ворог.
  3. Invalidation — це мистецтво вчасно викинути старе, щоб не отруїтися.
  4. Ви тепер знаєте, чому натискання F5 іноді магічно лагодить сайти (ви змушуєте браузер скинути свій кеш).

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

🔜 У наступній серії: Ми розібралися, як зберігати дані швидко. Але що робити, коли даних стає так багато, що вони не влазять в один сервер? Готуйтеся, ми поговоримо про Шардінг та Реплікацію. Це буде масштабно!

А поки що — спробуйте не їсти протухлу піцу! Побачимось! 👋