Logo Craft Homelab Docs Контакты Telegram
Caching стратегии — Cache-aside, write-through Трендовые github проекты в нашем телеграм канале. Подпишись 👉
Sun Feb 15 2026

Caching стратегии: Cache-aside, write-through

Производительность в современных приложениях часто определяется тем, насколько эффективно мы обходимся с повторяющимися дорогими операциями. Независимо от того, речь ли о запросах к базе данных, вызовах API или сложных вычислениях, правильная стратегия кэширования может дать порядок улучшения производительности. В этой статье мы разберем фундаментальные паттерны кэширования, которые должны быть в арсенале каждого разработчика: cache-aside, write-through и read-through.

Cache-aside: Ленивый подход к кэшированию

Cache-aside (также известный как lazy loading или lazy caching) — самый распространенный паттерн кэширования в современных приложениях. Здесь приложение явно управляет кэшем, проверяя его перед обращением к источнику данных и обновляя при получении новых данных.

Механика проста:

  1. При чтении данных сначала проверяем кэш
  2. Если есть в кэше (cache hit) — возвращаем данные
  3. Если нет в кэше (cache miss) — fetch’им из источника, сохраняем в кэш, возвращаем
  4. При записи обновляем и источник данных, и кэш
def get_user(user_id):
    # Сначала проверяем кэш
    user = cache.get(f"user_{user_id}")

    if user is None:
        # Cache miss - fetch из базы
        user = db.query("SELECT * FROM users WHERE id = %s", user_id)

        if user:
            # Сохраняем в кэш для будущих запросов
            cache.set(f"user_{user_id}", user, timeout=3600)

    return user

def update_user(user_id, new_data):
    # Сначала обновляем базу данных
    db.execute("UPDATE users SET name = %s, email = %s WHERE id = %s",
               new_data['name'], new_data['email'], user_id)

    # Инвалидируем или обновляем кэш
    cache.delete(f"user_{user_id}")

    # Альтернативный подход: обновляем кэш напрямую
    # user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    # cache.set(f"user_{user_id}", user, timeout=3600)

Узкие места в продакшене:

  1. Cache stampede - когда несколько потоков одновременно не находят данные в кэше и пытаются его populate’ить, что приводит к “громовому стаду”. Решение: реализовать блокировки или временно кэшировать null значения.
  2. Сложность инвалидации кэша: определить, когда инвалидировать запись, может быть нетривиально, особенно при наличии связей между данными.
  3. Нагрузка на память: без ограничения размера кэш может расти бесконечно, вызывая проблемы с памятью.

Write-through: Согласованность важнее скорости

Write-through обеспечивает запись данных одновременно и в кэш, и в источник данных. Это гарантирует согласованность между кэшем и источником данных, но ценой чуть более медленных записей.

Механика:

  1. При записи данных обновляем и кэш, и источник
  2. Чтение все равно идет через кэш
  3. При cache miss fetch’им из источника и populate’им кэш
class UserCache:
    def __init__(self, db_backend, cache_backend):
        self.db = db_backend
        self.cache = cache_backend

    def get_user(self, user_id):
        # Проверяем кэш
        user = self.cache.get(f"user_{user_id}")

        if user is None:
            # Cache miss - fetch из базы
            user = self.db.query("SELECT * FROM users WHERE id = %s", user_id)

            if user:
                # Сохраняем в кэш
                self.cache.set(f"user_{user_id}", user, timeout=3600)

        return user

    def update_user(self, user_id, new_data):
        # Записываем в базу
        self.db.execute("UPDATE users SET name = %s, email = %s WHERE id = %s",
                       new_data['name'], new_data['email'], user_id)

        # Одновременно записываем в кэш
        updated_user = self.db.query("SELECT * FROM users WHERE id = %s", user_id)
        self.cache.set(f"user_{user_id}", updated_user, timeout=3600)

Узкие места в продакшене:

  1. Задержка записи: каждая операция записи становится медленнее, так как нужно обновлять и кэш, и источник
  2. Сложности с инвалидацией в распределенных системах: убедиться, что все экземпляры кэша согласованы, может быть сложно
  3. Конкуренция ресурсов: высокая нагрузка на запись может привести к конкуренции между кэшем и источником данных

Read-through: Делегируем работу кэшу

Read-делегирует ответственность за populate кэш самому кэшу. При cache miss кэш самостоятельно fetch’ит данные из источника и populate’ит себя перед возвратом данных приложению.

Механика:

  1. Приложение взаимодействует только с кэшем
  2. При cache miss кэш сам fetch’ит данные из источника
  3. Кэш возвращает данные после populate’а
# Используем гипотетическую библиотеку read-through кэша
read_through_cache = ReadThroughCache(
    backend=RedisBackend(),
    data_loader=UserDatabaseLoader(),
    default_timeout=3600
)

def get_user(user_id):
    # Код приложения работает только с кэшем
    # При miss кэш автоматически загрузит из базы
    return read_through_cache.get(f"user_{user_id}")

def update_user(user_id, new_data):
    # Обновляем базу
    db.execute("UPDATE users SET name = %s, email = %s WHERE id = %s",
               new_data['name'], new_data['email'], user_id)

    # Инвалидируем кэш - при следующем чтении он repopulate'ится
    read_through_cache.delete(f"user_{user_id}")

Узкие места в продакшене:

  1. Зависимость от реализации кэша: кэш должен уметь загружать данные из источника
  2. Единая точка отказа: если кэш падает, приложение теряет доступ к данным
  3. Ограниченный контроль: приложение имеет меньше контроля над тем, когда и как populate’ится кэш

Гибридные подходы и лучшие практики

В реальных приложениях эти стратегии часто комбинируют или используют выборочно в зависимости от паттернов доступа к данным.

Распространенные гибридные подходы:

  1. Cache-aside для чтения и write-through для критичных данных
  2. Разное время жизни для разных типов данных
  3. Write-behind для записи с cache-aside для чтения

Ключевые соображения:

  1. Ограничение размера кэша: реализовывать политики eviction (LRU, LFU и т.д.)
  2. Warming кэша: предзагружать часто используемые данные при старте приложения
  3. Мониторинг и метрики: отслеживать hit/miss соотношения и адаптировать стратегии

Компромиссы:

  • Cache-aside: больше контроля, но требует тщательного управления инвалидацией
  • Write-through: сильная согласованность, но медленная запись
  • Read-through: проще код приложения, но сложнее реализация кэша

Заключение

Выбор стратегии кэширования зависит от конкретных требований вашего приложения к производительности, согласованности и сложности. Cache-aside предлагает гибкость и контроль, что идеально для приложений со сложными связями данных. Write-through обеспечивает согласованность ценой производительности записи, подходит для систем, где согласованность данных критична. Read-through упрощает код приложения, но добавляет сложности в реализацию кэша. На практике многие системы используют комбинацию этих стратегий, применяя разные паттерны к разным типам данных в зависимости от их паттернов доступа и важности.