← Назад к вопросам

Как обновить кэш в Redis?

1.8 Middle🔥 231 комментариев
#Архитектура систем

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как обновить кэш в 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

  1. Выбирать правильную стратегию — зависит от типа данных
  2. Установить разумный TTL — не слишком короткий (miss), не слишком длинный (старые данные)
  3. Использовать namespacingproduct:1, user:1, а не просто 1
  4. Мониторить hit rate — должен быть > 80%
  5. Обрабатывать cache miss — не допускать падения БД
  6. Использовать Redis transactions — для консистентности нескольких операций
  7. Сжимать большие значения — использовать gzip для экономии памяти

Сравнение стратегий

СтратегияКонсистентностьПроизводительностьСложностьКогда использовать
Write-ThroughВысокаяСредняяНизкаяКритичные данные
Write-BehindСредняяВысокаяВысокаяНекритичные данные
Cache-AsideНизкаяВысокаяНизкаяБольшинство случаев
TTLНизкаяВысокаяНизкаяВременные данные
Refresh-AheadВысокаяВысокаяВысокаяFrequently accessed

Вывод

Обновление кэша в Redis — это баланс между:

  • Консистентностью — насколько актуальны данные
  • Производительностью — как быстро система отвечает
  • Простотой — легко ли поддерживать код

Правильный выбор стратегии зависит от конкретного caso use и требований системы.

Как обновить кэш в Redis? | PrepBro