Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Как обновить кэш в Redis
Redis — это in-memory хранилище ключ-значение, используемое для кэширования данных. Обновление кэша в Redis — это одна из основных операций, когда данные изменяются и нужно обновить их в памяти. Есть несколько стратегий, каждая со своими плюсами и минусами.
Основные операции в Redis
Запись/Обновление значения
SET команда:
SET key value
SET product:1 '{"name": "Товар 1", "price": 100}'
SET с TTL (Time To Live):
SET key value EX 3600
SET product:1 '{...}' EX 3600 # Кэш истекает через 1 час
SET с условием (только если не существует):
SET key value NX # Only if Not eXists
SET product:1 '{...}' NX # Только если ключа ещё нет
SET с условием (только если существует):
SET key value XX # eXists
SET product:1 '{...}' XX # Только если ключ уже существует
Удаление значения
DEL команда:
DEL key
DEL product:1 # Удалить кэш товара
DEL product:* # Удалить все товары (требует SCAN)
UNLINK команда (неблокирующее удаление):
UNLINK key # Асинхронное удаление (быстрее для больших ключей)
UNLINK product:1
Стратегии обновления кэша
1. Write-Through (Синхронное обновление)
Принцип: Обновить БД → Обновить кэш
Процесс:
1. Приложение обновляет данные в БД
2. После успешного обновления БД, обновляется кэш
3. Если обновление кэша упало, можно откатить БД или потом пересчитать
Код (Python):
import redis
from sqlalchemy import create_engine
redis_client = redis.Redis(host='localhost', port=6379)
db = create_engine('postgresql://...')
def update_product(product_id, new_data):
# 1. Обновить в БД
with db.connect() as conn:
conn.execute(
f"UPDATE products SET name='{new_data['name']}' WHERE id={product_id}"
)
conn.commit()
# 2. Обновить в Redis
cache_key = f"product:{product_id}"
redis_client.set(cache_key, json.dumps(new_data), ex=3600)
return new_data
Плюсы:
- Кэш всегда актуален
- Простая логика
- Гарантированная консистентность
Минусы:
- Медленнее (два запроса вместо одного)
- Если кэш упал, может потерять данные
- Задержка на запись в кэш
2. Write-Behind (Asynchronous Cache Update)
Принцип: Обновить БД → Поставить задачу в очередь → Обновить кэш асинхронно
Процесс:
1. Обновить БД
2. Отправить событие в Message Queue (Kafka, RabbitMQ)
3. Consumer обновляет кэш асинхронно
Код с Celery:
from celery import shared_task
import redis
redis_client = redis.Redis()
def update_product(product_id, new_data):
# 1. Обновить в БД
product = Product.query.get(product_id)
product.name = new_data['name']
db.session.commit()
# 2. Поставить задачу на обновление кэша
update_cache_async.delay(product_id, new_data)
return new_data
@shared_task
def update_cache_async(product_id, data):
"""Асинхронно обновляет кэш"""
cache_key = f"product:{product_id}"
redis_client.set(cache_key, json.dumps(data), ex=3600)
Плюсы:
- Быстро (не ждём обновления кэша)
- Высокая производительность
- Кэш обновляется в фоне
Минусы:
- Временный gap между БД и кэшем
- Сложнее отладить
- Может быть дублирование обновлений
3. Cache Invalidation (Инвалидация кэша)
Принцип: Обновить БД → Удалить старый кэш → При следующем запросе загрузить из БД
Процесс:
1. Обновить БД
2. Удалить из Redis (DEL key)
3. При следующем запросе: если ключа нет в Redis → загрузить из БД
Код:
def update_product(product_id, new_data):
# 1. Обновить в БД
product = Product.query.get(product_id)
product.name = new_data['name']
db.session.commit()
# 2. Инвалидировать кэш
cache_key = f"product:{product_id}"
redis_client.delete(cache_key)
return new_data
def get_product(product_id):
cache_key = f"product:{product_id}"
# Проверить кэш
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Если не в кэше, загрузить из БД
product = Product.query.get(product_id)
redis_client.set(cache_key, json.dumps(product.to_dict()), ex=3600)
return product.to_dict()
Плюсы:
- Проще реализовать
- Избежать неактуального кэша
- Быстрое удаление
Минусы:
- При удалении могут быть одновременные запросы (cache stampede)
- Больше нагрузки на БД при miss
4. TTL (Time To Live) - Автоматическое истечение
Принцип: Установить время жизни кэша, после чего он автоматически удалится
Код:
def get_product(product_id):
cache_key = f"product:{product_id}"
# SET с TTL 1 час
redis_client.set(cache_key, json.dumps(data), ex=3600)
# Или SETEX
redis_client.setex(cache_key, 3600, json.dumps(data))
# Или PSETEX (миллисекунды)
redis_client.psetex(cache_key, 3600000, json.dumps(data))
Плюсы:
- Простое решение
- Не нужно вручную удалять
- Предсказуемое поведение
Минусы:
- Кэш может быть старым в конце периода
- Нужно выбрать правильный TTL
Паттерны обновления
Pattern 1: Cache-Aside (Lazy Loading)
Логика:
Взять из кэша?
├─ Да → вернуть
└─ Нет → взять из БД → сохранить в кэш → вернуть
Код:
def get_user_with_cache(user_id):
cache_key = f"user:{user_id}"
# 1. Попробовать из кэша
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# 2. Если нет, загрузить из БД
user = User.query.get(user_id)
if not user:
return None
# 3. Сохранить в кэш
user_data = user.to_dict()
redis_client.setex(cache_key, 3600, json.dumps(user_data))
return user_data
Плюсы: Простой, не требует внешнего обновления Минусы: Первый запрос медленнее, может быть cache miss
Pattern 2: Write-Through
Логика:
Обновление:
1. Обновить БД
2. Обновить кэш
Чтение:
1. Взять из кэша
2. Если нет → взять из БД
Pattern 3: Refresh-Ahead
Логика:
Если кэш вот-вот истечёт (< 10% от TTL):
→ Автоматически обновить из БД в фоне
Иначе:
→ Вернуть из кэша
Код:
def get_product_with_refresh(product_id):
cache_key = f"product:{product_id}"
ttl_key = f"{cache_key}:ttl"
# Получить TTL оставшегося времени
ttl = redis_client.ttl(cache_key)
# Если кэш вот-вот истечёт (менее 10% времени осталось)
if ttl > 0 and ttl < 360: # Из 3600 сек осталось < 10%
# Обновить в фоне
refresh_cache_async.delay(product_id)
# Вернуть текущее значение (может быть старым)
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Если не в кэше, загрузить и сохранить
product = Product.query.get(product_id)
redis_client.setex(cache_key, 3600, json.dumps(product.to_dict()))
return product.to_dict()
Плюсы: Кэш всегда свежий, нет cache miss
Обновление нескольких ключей (Batch Update)
Сценарий: Обновить товар → обновить кэш товара И кэш категории И кэш списка
def update_product_batch(product_id, new_data):
# 1. Обновить в БД
product = Product.query.get(product_id)
product.name = new_data['name']
db.session.commit()
# 2. Инвалидировать все связанные кэши
keys_to_delete = [
f"product:{product_id}",
f"category:{product.category_id}:products",
f"products:list",
f"products:search:*",
f"user:*:recommendations",
]
# Удалить все ключи
if keys_to_delete:
redis_client.delete(*keys_to_delete)
# Или удалить по паттерну
keys_pattern = redis_client.keys("products:*")
if keys_pattern:
redis_client.delete(*keys_pattern)
Решение проблемы Cache Stampede
Проблема: Когда кэш истекает, множество запросов одновременно идёт в БД.
Решение 1: Distributed Lock
import time
def get_product_safe(product_id):
cache_key = f"product:{product_id}"
lock_key = f"{cache_key}:lock"
# Попробовать получить из кэша
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Попробовать установить блокировку
if redis_client.set(lock_key, "1", nx=True, ex=5): # 5 сек lock
try:
# Загрузить из БД
product = Product.query.get(product_id)
redis_client.setex(cache_key, 3600, json.dumps(product.to_dict()))
return product.to_dict()
finally:
redis_client.delete(lock_key)
else:
# Другой процесс обновляет, подождать и повторить
time.sleep(0.1)
return get_product_safe(product_id)
Решение 2: Probabilistic early expiration
import random
def get_product_probabilistic(product_id):
cache_key = f"product:{product_id}"
cached = redis_client.get(cache_key)
if cached:
ttl = redis_client.ttl(cache_key)
# Если кэш близко к истечению, небольшой % обновляют рано
if ttl > 0 and ttl < 600 and random.random() < 0.1: # 10% вероятность
refresh_cache_async.delay(product_id)
return json.loads(cached)
product = Product.query.get(product_id)
redis_client.setex(cache_key, 3600, json.dumps(product.to_dict()))
return product.to_dict()
Мониторинг кэша
def log_cache_stats():
info = redis_client.info('stats')
hits = info.get('keyspace_hits', 0)
misses = info.get('keyspace_misses', 0)
hit_rate = hits / (hits + misses) if (hits + misses) > 0 else 0
print(f"Cache Hit Rate: {hit_rate:.2%}")
print(f"Total Keys: {redis_client.dbsize()}")
print(f"Memory Used: {info.get('used_memory_human', 'N/A')}")
Best Practices
- Выбирать правильную стратегию — зависит от типа данных
- Установить разумный TTL — не слишком короткий (miss), не слишком длинный (старые данные)
- Использовать namespacing —
product:1,user:1, а не просто1 - Мониторить hit rate — должен быть > 80%
- Обрабатывать cache miss — не допускать падения БД
- Использовать Redis transactions — для консистентности нескольких операций
- Сжимать большие значения — использовать gzip для экономии памяти
Сравнение стратегий
| Стратегия | Консистентность | Производительность | Сложность | Когда использовать |
|---|---|---|---|---|
| Write-Through | Высокая | Средняя | Низкая | Критичные данные |
| Write-Behind | Средняя | Высокая | Высокая | Некритичные данные |
| Cache-Aside | Низкая | Высокая | Низкая | Большинство случаев |
| TTL | Низкая | Высокая | Низкая | Временные данные |
| Refresh-Ahead | Высокая | Высокая | Высокая | Frequently accessed |
Вывод
Обновление кэша в Redis — это баланс между:
- Консистентностью — насколько актуальны данные
- Производительностью — как быстро система отвечает
- Простотой — легко ли поддерживать код
Правильный выбор стратегии зависит от конкретного caso use и требований системы.