Кешування з Redis: cache-aside, TTL та інвалідація без болю
Як додати кеш, який справді прискорює сервіс і не віддає застарілі дані: патерни кешування, вибір TTL, захист від «лавини» запитів і типові помилки.
У комп'ютерних науках дві складні речі: інвалідація кешу і придумування назв. Кеш легко додати і дуже легко зробити джерелом багів: користувач змінив профіль, а бачить старі дані ще годину. Розберемо, як робити це правильно, на прикладі Redis і Python.
Cache-aside — патерн за замовчуванням
Застосунок сам вирішує, коли читати з кешу і коли з бази:
- Шукаємо в кеші.
- Знайшли — повертаємо.
- Не знайшли — читаємо з бази, кладемо в кеш з TTL, повертаємо.
import asyncio
import json
import redis.asyncio as redis
r = redis.Redis(host="cache", decode_responses=True)
async def get_product(product_id: int) -> dict:
key = f"product:v2:{product_id}"
cached = await r.get(key)
if cached is not None:
return json.loads(cached)
product = await db.fetch_product(product_id)
if product is None:
await r.set(key, json.dumps(None), ex=60) # кешуємо і «немає», але коротко
return None
await r.set(key, json.dumps(product), ex=600)
return productПорадаДодавайте версію в ключ (
product:v2:...). Змінили формат даних у кеші — збільшили версію, і старі записи просто «зникнуть» з ужитку, не ламаючи новий код.
Інвалідація: видаляти, а не оновлювати
При зміні даних видаляйте ключ, а не записуйте нове значення:
async def update_product(product_id: int, data: dict):
await db.update_product(product_id, data)
await r.delete(f"product:v2:{product_id}")Чому не set нового значення? Два паралельні оновлення можуть записати в кеш у «неправильному» порядку, і там надовго залишиться застаріла версія. Видалення безпечніше: наступне читання візьме актуальні дані з бази.
УвагаВидаляйте з кешу після успішного коміту транзакції в базі. Якщо видалити до коміту, паралельний запит може встигнути прочитати старі дані з бази і знову покласти їх у кеш.
TTL — страховка, а не стратегія
Навіть з явною інвалідацією ставте TTL: якщо десь забули видалити ключ, застарілі дані проживуть обмежений час.
| Дані | Типовий TTL |
|---|---|
| Довідники, налаштування | 10–60 хв |
| Картки товарів, профілі | 5–15 хв + явна інвалідація |
| Результати важких звітів | за розкладом перерахунку |
| Сесії | час життя сесії |
| Лічильники, rate limit | вікно ліміту |
Додавайте випадковий розкид до TTL (600 ± 60 секунд), щоб тисячі ключів, створених одночасно, не протухли в одну мить.
Лавина запитів (cache stampede)
Популярний ключ протух — і 500 запитів одночасно йдуть у базу рахувати одне й те саме. Захист — короткий лок: перерахунок робить лише один запит.
async def get_with_lock(key: str, loader, ttl: int = 600):
cached = await r.get(key)
if cached is not None:
return json.loads(cached)
lock_key = f"lock:{key}"
if await r.set(lock_key, "1", nx=True, ex=10): # отримали лок
try:
value = await loader()
await r.set(key, json.dumps(value), ex=ttl)
return value
finally:
await r.delete(lock_key)
for _ in range(50): # інші чекають на результат
await asyncio.sleep(0.1)
cached = await r.get(key)
if cached is not None:
return json.loads(cached)
return await loader() # запасний варіантЩо не варто кешувати
- дані, які змінюються частіше, ніж читаються;
- персональні дані без потреби — кеш теж треба захищати й чистити;
- результати з помилками (
500від зовнішнього API) — інакше помилка «залипне» на весь TTL.
Налаштування Redis для кешу
maxmemory 2gb
maxmemory-policy allkeys-lfu # витісняти найрідше використовувані ключіВажливоЯкщо Redis використовується лише як кеш, застосунок має працювати і без нього — просто повільніше. Обгорніть звернення до кешу в
try/exceptз таймаутом: недоступний кеш не повинен класти сервіс.
Як зрозуміти, що кеш працює
- hit ratio = hits / (hits + misses) з
INFO stats(keyspace_hits,keyspace_misses); - латентність ендпоінтів до і після;
- навантаження на базу.
Hit ratio 30% — кеш, імовірно, кешує не те або з занадто коротким TTL. 95%+ для довідкових даних — нормально.
Чому при оновленні даних краще видалити ключ із кешу, ніж записати туди нове значення?
При паралельних оновленнях записи в кеш можуть прийти в іншому порядку, ніж коміти в базу, і в кеші залишиться застаріла версія до кінця TTL. Видалення змушує наступне читання взяти актуальні дані з бази.
Збережіть статтю собі
Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.
Увійти через Google Інші способи входу