[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"question-keshirovanie-kak-obespechit-soglasovannost-kesha-v-raspredelyonnoy-sisteme":3},{"id":4,"slug":5,"topicId":6,"topicSlug":7,"topicName":8,"topicEmoji":9,"question":10,"answer":11,"codeLang":12,"codeSrc":12,"important":12,"commonMistakes":12,"modernUsage":12,"difficulty":13,"tags":14,"related":21,"progress":22,"seo":23},188,"kak-obespechit-soglasovannost-kesha-v-raspredelyonnoy-sisteme",5,"keshirovanie","Кеширование","⚡","Как обеспечить согласованность кэша в распределённой системе?","Согласованность кэша в распределённой системе — это обеспечение того, чтобы все экземпляры приложения и микросервисы видели актуальные данные, несмотря на наличие локальных и распределённых кэшей.\n\n> **Аналогия из жизни:** представьте, что у каждого филиала компании есть свой прайс-лист (L1-кэш). Когда центральный офис (БД) меняет цены, нужен механизм оповещения всех филиалов — иначе клиенты в разных городах увидят разные цены.\n\n### Проблемы\n\n- Экземпляр A обновил БД и инвалидировал свой L1-кэш, но экземпляр B отдаёт старые данные из своего L1\n- Сервис A обновил пользователя, но сервис B кэширует старую версию\n- Race condition: два запроса одновременно обновляют кэш\n\n### Стратегии обеспечения согласованности\n\n### 1. Только Redis (без L1)\n\nСамый простой вариант — все читают\u002Fпишут в один Redis. Согласованность гарантирована.\n\n**Минус:** каждое чтение — сетевой вызов (~1 мс).\n\n### 2. Redis Pub\u002FSub для инвалидации L1\n\n\u003Cdetails>\u003Csummary>Пример кода\u003C\u002Fsummary>\n\n```java\n\u002F\u002F При обновлении — publish в канал\npublic void updateUser(User user) {\n    userRepository.save(user);\n    cache.invalidate(user.getId());                    \u002F\u002F свой L1\n    redis.delete(\"user:\" + user.getId());              \u002F\u002F L2\n    redis.convertAndSend(\"cache:invalidate\", \"user:\" + user.getId()); \u002F\u002F все L1\n}\n\n\u002F\u002F Все экземпляры подписаны на канал\n@Component\npublic class CacheInvalidationSubscriber implements MessageListener {\n    private final Cache\u003CLong, User> localCache;\n\n    @Override\n    public void onMessage(Message message, byte[] pattern) {\n        String key = new String(message.getBody()); \u002F\u002F \"user:42\"\n        Long id = Long.parseLong(key.split(\":\")[1]);\n        localCache.invalidate(id);\n    }\n}\n```\n\n\u003C\u002Fdetails>\n\n### 3. Event-driven через Kafka\n\n\u003Cdetails>\u003Csummary>Пример кода\u003C\u002Fsummary>\n\n```java\n\u002F\u002F Сервис A → Kafka → Сервисы B, C, D\n@TransactionalEventListener(phase = AFTER_COMMIT)\npublic void onUserUpdated(UserUpdatedEvent event) {\n    kafkaTemplate.send(\"user-cache-invalidation\", event.getUserId().toString());\n}\n\n\u002F\u002F Каждый сервис-потребитель\n@KafkaListener(topics = \"user-cache-invalidation\")\npublic void handleInvalidation(String userId) {\n    cacheManager.getCache(\"users\").evict(Long.parseLong(userId));\n}\n```\n\n\u003C\u002Fdetails>\n\n### 4. Короткий TTL для L1\n\nСамый простой компромисс: L1 TTL = 30 секунд. Данные обновятся максимум через 30 секунд без дополнительной инфраструктуры.\n\n### Сравнение стратегий\n\n| Стратегия | Задержка обновления | Сложность | Инфраструктура |\n|-----------|-------------------|-----------|---------------|\n| Только Redis (без L1) | 0 | Низкая | Redis |\n| Redis Pub\u002FSub | Миллисекунды | Средняя | Redis |\n| Kafka events | Секунды | Высокая | Kafka |\n| Короткий TTL | До TTL секунд | Низкая | Ничего |\n\n### Ключевые принципы\n\n- Строгая согласованность (strong consistency) невозможна с кэшем — кэш по определению eventual consistent\n- Приемлемая задержка — ключевой вопрос: 30 секунд устаревших данных допустимо для каталога, но не для баланса\n- Для критичных данных (баланс, инвентарь) — не кэшировать или использовать только Redis с Write-Through\n\n### Частые ошибки\n\n- **Кэшировать мутабельные критичные данные** — баланс счёта, количество товара не должны кэшироваться в L1\n- **Redis Pub\u002FSub без обработки пропусков** — если экземпляр был недоступен, он пропустит сообщение; TTL как fallback обязателен\n- **Полагаться только на TTL** — для важных данных TTL 10 минут = до 10 минут stale data\n\n### Как используется в 2026\n\n- Redis Pub\u002FSub для L1-инвалидации — простой и эффективный подход\n- Kafka для межсервисной инвалидации — стандарт в микросервисах\n- Короткий L1 TTL (30с-2мин) — pragmatic подход, когда Pub\u002FSub избыточен\n\n> **На собеседовании:** интервьюер проверяет опыт работы с распределёнными системами. Ключевой ответ: строгая согласованность с кэшем невозможна, поэтому нужно выбрать приемлемый уровень eventual consistency. Частая ошибка — предложить только TTL и не упомянуть Pub\u002FSub или Kafka.","","senior",[15,16,17,18,19,20],"invalidation","distributed-systems","kafka","pub-sub","caching","consistency",[],null,{"title":24,"description":25,"ogTitle":26,"ogDescription":27,"keywords":28,"schemaAnswer":37,"featuredSnippetReady":38},"Как обеспечить согласованность кэша в распределённой системе — Gymterview","Согласованность кэша в распределённой системе: Redis Pub\u002FSub, Kafka-инвалидация, короткий L1 TTL. Стратегии для микросервисов и multi-instance приложений.","Согласованность кэша в распределённой системе — Gymterview","Стратегии обеспечения согласованности кэша: только Redis, Pub\u002FSub, Kafka-events, короткий L1 TTL. Примеры на Spring.",[29,30,31,32,33,34,35,36],"согласованность кэша","cache consistency","распределённая система","Redis Pub\u002FSub","Kafka инвалидация","L1 кэш","микросервисы","Java","Стратегии: 1) Только Redis без L1 — все читают из одного источника; 2) Redis Pub\u002FSub — при обновлении публикуется сообщение, все экземпляры инвалидируют L1; 3) Event-driven через Kafka — для межсервисной инвалидации; 4) Короткий L1 TTL (30с-2мин) — простой компромисс. Строгая согласованность с кэшем невозможна — кэш по определению eventual consistent. Критичные данные (баланс) не кэшировать.",true]