Налаштування стратегії інвалідації кэша (TTL, Event-Based, Cache-Aside)
Кэширование без продуманої інвалідації — джерело важко відстежуваних багів зі застарілими даними. Вибір стратегії залежить від вимог до свіжості даних та архітектури системи.
Основні стратегії
TTL (Time-To-Live) — дані автоматично застарівають через заданий проміжок. Просто реалізувати, але дані можуть бути застарілими до закінчення TTL.
Cache-Aside (Lazy Loading) — додаток спочатку перевіряє кэш, при miss завантажує з БД та записує у кэш. Найпоширеніша стратегія.
Write-Through — запис одночасно у кэш та БД. Дані завжди свіжі, але кожна запис проходить через кэш.
Event-Based Invalidation — при змінах даних генерується подія, яка інвалідує відповідні ключі кэша.
Cache-Aside з TTL
import redis
import json
from functools import wraps
redis_client = redis.Redis(host='redis', decode_responses=True)
def cached(key_template, ttl=300):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = key_template.format(*args, **kwargs)
cached_val = redis_client.get(cache_key)
if cached_val:
return json.loads(cached_val)
result = func(*args, **kwargs)
redis_client.setex(cache_key, ttl, json.dumps(result))
return result
return wrapper
return decorator
@cached("user:{0}", ttl=600)
def get_user(user_id):
return db.query("SELECT * FROM users WHERE id = %s", user_id)
Інвалідація при оновленні:
def update_user(user_id, data):
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
redis_client.delete(f"user:{user_id}")
# Інвалідуємо пов'язані ключі
redis_client.delete(f"user_posts:{user_id}")
redis_client.delete(f"user_profile_full:{user_id}")
Event-Based інвалідація через чергу
# publisher (при змінах даних)
import pika
def publish_invalidation(entity_type, entity_id, changed_fields=None):
connection = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq'))
channel = connection.channel()
channel.exchange_declare(exchange='cache_invalidation', exchange_type='topic')
message = json.dumps({
'entity': entity_type,
'id': entity_id,
'fields': changed_fields
})
channel.basic_publish(
exchange='cache_invalidation',
routing_key=f'invalidate.{entity_type}',
body=message
)
# subscriber (кэш-сервіс)
def on_user_changed(channel, method, properties, body):
event = json.loads(body)
patterns_to_invalidate = [
f"user:{event['id']}",
f"user_full:{event['id']}",
]
if 'role' in (event.get('fields') or []):
patterns_to_invalidate.append(f"user_permissions:{event['id']}")
for key in patterns_to_invalidate:
redis_client.delete(key)
Cache Tags (залежності)
Тегування дозволяє інвалідувати групи пов'язаних ключів за одним тегом:
// PHP/Laravel: Spatie Response Cache або кастомна реалізація
class TaggedCache
{
public function put(string $key, $value, int $ttl, array $tags = []): void
{
Redis::setex($key, $ttl, serialize($value));
foreach ($tags as $tag) {
Redis::sadd("cache_tag:{$tag}", $key);
Redis::expire("cache_tag:{$tag}", $ttl + 60);
}
}
public function invalidateByTag(string $tag): void
{
$keys = Redis::smembers("cache_tag:{$tag}");
if (!empty($keys)) {
Redis::del($keys);
}
Redis::del("cache_tag:{$tag}");
}
}
// Використання
$cache->put("product:42", $product, 3600, ['product:42', 'category:5', 'brand:3']);
// При змінах категорії 5 — інвалідуємо все пов'язане
$cache->invalidateByTag('category:5');
Stale-While-Revalidate
Паттерн: повертати застарілі дані, поки фоново оновлюється кэш. Усуває cache stampede:
import threading
def get_with_stale_revalidate(key, fetch_fn, ttl=300, stale_ttl=60):
data = redis_client.get(key)
if data:
result = json.loads(data)
remaining_ttl = redis_client.ttl(key)
# Якщо TTL мало — почати фонове оновлення
if remaining_ttl < stale_ttl:
lock_key = f"revalidate_lock:{key}"
if redis_client.set(lock_key, 1, nx=True, ex=30):
threading.Thread(
target=lambda: _background_refresh(key, fetch_fn, ttl)
).start()
return result
# Cache miss — синхронне отримання
result = fetch_fn()
redis_client.setex(key, ttl, json.dumps(result))
return result
def _background_refresh(key, fetch_fn, ttl):
try:
result = fetch_fn()
redis_client.setex(key, ttl, json.dumps(result))
finally:
redis_client.delete(f"revalidate_lock:{key}")
Захист від Cache Stampede через Locks
def get_with_lock(key, fetch_fn, ttl=300):
result = redis_client.get(key)
if result:
return json.loads(result)
lock = redis_client.lock(f"lock:{key}", timeout=10)
if lock.acquire(blocking=True, blocking_timeout=5):
try:
# Повторна перевірка після отримання блокування
result = redis_client.get(key)
if result:
return json.loads(result)
data = fetch_fn()
redis_client.setex(key, ttl, json.dumps(data))
return data
finally:
lock.release()
TTL стратегії за типом даних
| Тип даних | TTL | Інвалідація |
|---|---|---|
| Профіль користувача | 10 хв | При оновленні |
| Список товарів | 5 хв | При змінах товару |
| Конфіг додатку | 1 год | При деплою |
| Курси валют | 30 сек | По подіям |
| Права користувача | 5 хв | При змінах ролі |
| HTML-сторінки | 1 год | При публікації |
Мониторинг ефективності кэша
# Redis INFO stats
redis-cli INFO stats | grep -E "keyspace_hits|keyspace_misses"
# keyspace_hits:12847293
# keyspace_misses:234821
# Hit rate = hits / (hits + misses)
# Нормально: > 80%
Prometheus метрика:
redis_keyspace_hits_total / (redis_keyspace_hits_total + redis_keyspace_misses_total)
Строки виконання
Розробка стратегії інвалідації з Cache Tags та Event-Based підходом — 3–5 робочих днів.







