Кэширование и согласованность данных#
Содержимое главы#
- Зачем кеширование в распределённых системах
- Типы кешей: локальные и распределённые
- Стратегии кеширования: Cache-aside, write-through, write-back
- TTL и управление временем жизни данных
- Инвалидация кешей
- Согласованность кеша и источника данных
- Типичные ошибки и ложные оптимизации
Зачем кеширование в распределённых системах#
Кеширование - это не только про ускорение, а про снижение нагрузки и повышение устойчивости.
В распределённой системе:
- Каждый сетевой вызов дорог
- Каждая БД имеет предел по QPS
- Каждый внешний сервис может деградировать
Кеш решает сразу несколько задач:
- Снижает нагрузку на БД и сервисы-источники
- Уменьшает задержки (особенно p95 / p99)
- Сглаживает пики нагрузки
- Позволяет системе выживать при частичных отказах зависимостей
Где и что обычно кешируют#
В реальных системах кеш почти всегда появляется вокруг операций чтения. Чаще всего кешируют профили пользователей, заказы, каталоги, справочные данные и агрегаты - всё, что читается значительно чаще, чем изменяется. Также кешируют результаты тяжёлых вычислений и ответы внешних API, чтобы защитить систему от повторных вызовов.
При этом кеш редко используют для критичных write-операций или данных, где цена ошибки слишком высока, например, для балансов или финансовых транзакций. Там, где важна строгая консистентность, кеш либо не используется вовсе, либо применяется очень аккуратно и локально.
Типы кешей: локальный и распределённый#
Самый простой вариант кеша - локальный, находящийся прямо в памяти процесса сервиса. Он даёт минимальные задержки, не требует сетевых вызовов и легко реализуется. Такой кеш хорошо подходит для горячих данных и как дополнительный уровень защиты от повторяющихся запросов. Однако у него есть фундаментальное ограничение: каждый инстанс сервиса имеет свой собственный кеш. Это означает отсутствие общей картины и потерю кеша при рестарте.
Распределённый кеш, например Redis, выносится в отдельный сервис и используется всеми инстансами приложения. Он позволяет централизованно управлять TTL, делиться данными между сервисами и масштабироваться независимо. За это приходится платить сетевой задержкой и сложностью инфраструктуры. На практике почти всегда используется комбинация: локальный кеш для самых горячих данных и Redis как общий слой.
Стратегии кеширования#
Наиболее распространённая стратегия - cache-aside. В этой модели приложение сначала пытается прочитать данные из кеша. Если данных нет, оно обращается к базе, а затем сохраняет результат в кеш. Эта стратегия проста, не затрагивает write-путь и полностью контролируется приложением. Именно она используется в большинстве production-систем.
Другие стратегии встречаются значительно реже. Write-through предполагает, что запись идёт сначала в кеш, а кеш уже синхронно пишет данные в базу. Это упрощает чтение, но делает кеш частью критического пути записи. Write-back (или write-behind) ещё опаснее: данные сначала записываются в кеш, а в базу уходят асинхронно. Такой подход даёт высокую производительность, но создаёт риск потери данных и почти всегда считается антипаттерном для бизнес-систем.
Стратегии замещения данных (Eviction policies)#
Любой кеш в распределённой системе имеет ограничённый объём памяти, поэтому рано или поздно возникает необходимость удалять старые данные, чтобы освободить место для новых. Стратегия замещения (eviction policy) определяет, какие элементы удалять при заполнении кеша. Выбор этой стратегии напрямую влияет на эффективность кеша, нагрузку на базу данных и скорость отклика системы. Основные подходы включают LRU (Least Recently Used) - удаление наименее недавно использованных данных, LFU (Least Frequently Used) - удаление наименее часто используемых, FIFO (First In, First Out) - удаление самых старых элементов и TTL-based eviction - удаление по истечении заданного времени жизни.
На практике часто используют гибридные стратегии, например LRU с TTL, чтобы одновременно учитывать частоту доступа и свежесть данных. Выбор подходящей политики зависит от характера нагрузки и паттернов использования: LRU хорошо подходит для API с повторяющимися запросами, LFU - для «горячих» справочников, TTL - для временных или внешних данных. Важно понимать, что удаление данных из кеша - это рабочая ситуация и неправильно выбранная стратегия может вызвать избыточные обращения к источнику данных и падение производительности.
При проектировании кеша стоит учитывать не только политику замещения, но и схему кеширования (cache-aside, write-through, write-back), время жизни данных и требования к консистентности. Стратегия замещения должна быть частью общей архитектуры кеширования, чтобы обеспечить оптимальный hit ratio, минимизировать задержки и сохранить согласованность между кешем и источником данных.
TTL и управление временем жизни данных#
TTL - основной механизм, с помощью которого система борется с устаревшими данными. Он определяет, как долго запись может находиться в кеше, прежде чем будет удалена. Слишком маленький TTL приводит к постоянным cache miss и лишней нагрузке на базу. Слишком большой - к длительному использованию устаревших данных.
На практике TTL подбирается индивидуально для разных типов данных. Часто применяют jitter - небольшое случайное отклонение времени жизни, чтобы избежать одновременного истечения большого числа ключей и всплесков нагрузки. В более сложных системах используется подход stale-while-revalidate, когда клиент получает слегка устаревшие данные, а обновление происходит в фоне.
Инвалидация кешей#
Инвалидация кеша считается одной из самых сложных задач в разработке, и неслучайно. Самый простой подход - вообще ничего не инвалидировать и полагаться только на TTL. Это нормально работает в системах, готовых жить с eventual consistency.
Явная инвалидация, когда при обновлении данных ключ удаляется из кеша, выглядит логично, но в распределённой среде быстро приводит к race conditions и ошибкам. Более масштабируемый подход - инвалидация через события, когда сервис реагирует на доменные события и обновляет или очищает кеш асинхронно. Такой подход лучше ложится на event-driven архитектуру, но всё равно не гарантирует мгновенную согласованность.
Согласованность кеша и источника данных#
Важно чётко зафиксировать: кеш никогда не является источником истины. Истина всегда находится в базе данных или в домене-владельце данных. Кеш - это оптимизация, а не часть модели данных. В распределённых системах нужно уметь жить с тем, что разные клиенты в разное время видят разные версии данных. Запись могла уже произойти, но кеш ещё не обновился. Кеш мог обновиться, а транзакция в базе - откатиться. Задача архитектора не в том, чтобы полностью устранить эти расхождения, а в том, чтобы сделать их безопасными для бизнеса.
Типичные ошибки и ложные оптимизации#
Одна из самых распространённых ошибок - начинать кешировать «на всякий случай». Кеширование без понимания целей почти всегда приводит к усложнению системы без реальной пользы. Часто кеш пытаются использовать как вторую базу данных, забывают про TTL или строят сложную логику инвалидации там, где достаточно было допустить небольшую задержку обновления данных. Хороший кеш - это простой кеш. Он легко отключается, не влияет на корректность системы и не требует сложных рассуждений для понимания его поведения
Дополнительные источники#
- Distributed system caching
- Redis Works as a Cache
- Анализ стратегий кеширования
- Cache Patterns
- Cache stampede
- Google SRE Book - Distributed Caching
- Designing Data-Intensive Applications - Martin Kleppmann