Кеш в PostgreSQL-приложении: стратегии записи и инвалидации
Кеш рядом с PostgreSQL ускоряет чтение, но добавляет второй источник устаревших данных. Разбираем cache-aside, write-through и способы инвалидации: где они помогают, как возникают гонки и что выбрать для конкретной нагрузки.

Кеш рядом с PostgreSQL может ускорить чтение, но простое удаление ключа после записи не гарантирует свежих данных: параллельный запрос способен положить в кеш старую версию уже после инвалидации. Ниже — как устроены cache-aside и write-through, какие способы инвалидации с ними сочетаются и чем вы платите за каждую стратегию. Рассматриваем приложение с PostgreSQL 16 и внешним кешем; конкретный продукт кеша не предполагаем.
Сначала разделите два разных кеша
PostgreSQL уже использует память для кеширования страниц данных, а операционная система — для файлового кеша. Это не то же самое, что прикладной кеш с ключами вроде «карточка товара» или «профиль пользователя». Внешний кеш — например, Redis — хранит подготовленные приложением значения и требует отдельной логики записи, срока жизни и инвалидации.
Не стоит начинать с предположения, что база «не справляется». Сначала проверьте планы запросов, индексы и статистику, затем измерьте задержку и нагрузку. Если запрос читает одни и те же данные часто, а данные можно безопасно переиспользовать, кеш может помочь. Если узкое место — запись, блокировки или запросы, которые каждый раз возвращают уникальные результаты, кеш не устранит причину.
Термин. Инвалидация — удаление или пометка устаревшей кешированной записи, чтобы следующий читатель получил актуальные данные из источника. TTL — срок жизни записи: по его истечении кеш считает её недействительной, даже если приложение не прислало отдельную команду.
Cache-aside: приложение управляет чтением
При cache-aside приложение сначала ищет значение в кеше. При промахе читает PostgreSQL, сохраняет результат в кеше и возвращает его клиенту. При записи приложение обновляет базу, затем удаляет или обновляет соответствующий ключ кеша.
Преимущество — простой путь записи в базу и кеширование только тех данных, которые действительно запросили. Цена — приложение должно правильно обработать промахи, ошибки кеша и инвалидацию. После удаления ключа первый запрос снова нагрузит PostgreSQL; если одновременно придёт много таких запросов, они могут все пойти в базу.
У cache-aside есть неприятная гонка. Читатель не нашёл ключ и начал получать старое значение из базы. В это время писатель сохранил новое значение и удалил ключ. Затем читатель завершил старый запрос и записал прежние данные в кеш. Ключ снова существует, но содержит устаревшую версию.
Обычный TTL ограничивает время, в течение которого ошибка продержится, но не предотвращает её. Если устаревшее значение недопустимо, нужно контролировать порядок версий или проектировать чтение и запись так, чтобы старый результат не мог перезаписать новый.
Write-through: обновление кеша входит в путь записи
Термин используют по-разному. Здесь write-through означает, что при изменении приложение синхронно обновляет основную запись в PostgreSQL и соответствующее значение во внешнем кеше, прежде чем считать операцию завершённой. PostgreSQL остаётся источником истины; кеш не должен подтверждать изменение, которое не попало в базу.
Читатели получают данные из кеша, а после успешной записи новая версия сразу становится доступна там же. Это уменьшает окно устаревания при обычном обновлении. Но появляются две операции и два места отказа: база может принять запись, а кеш — быть недоступен. Тогда приложение должно решить, считать ли бизнес-операцию успешной, повторять обновление кеша или отправлять событие на повторную обработку.
Транзакция PostgreSQL не включает внешнее хранилище автоматически. Поэтому нельзя считать обновление базы и кеша одной атомарной операцией. Если сначала записать в кеш, а транзакция базы откатится, кеш будет показывать несуществующее изменение. Если сначала записать в базу, а кеш не обновится, временно останется старая версия.
Кеш не становится согласованным от того, что вы обновляете его «сразу после базы». Важно, что происходит при сбое между двумя операциями и кто исправляет расхождение.
Как выбрать инвалидацию
| Подход | Что происходит | Цена и риск |
|---|---|---|
| TTL | Запись исчезает по истечении заданного времени | Просто внедрить; до истечения TTL читатель может получить устаревшее значение |
| Удаление или обновление ключа при записи | Приложение сразу воздействует на связанные кеш-ключи | Быстро при успехе; сбой между записью в PostgreSQL и кеш вызывает расхождение |
| Версионирование ключей | Новая версия данных получает новый ключ, старый постепенно истекает | Старое значение не перезаписывает актуальный ключ; нужно надёжно получать текущую версию |
| Очередь событий и outbox | Запись и событие фиксируются в одной транзакции, обработчик позже обновляет или удаляет кеш | События можно повторять; обновление становится асинхронным, нужны мониторинг задержки и защита от повторной обработки |
Для некритичных витринных данных часто достаточно cache-aside с TTL и явным удалением ключа при записи. Для данных, где важно быстро отражать изменение, можно добавить синхронное обновление или короткий путь доставки событий. Если потеря события недопустима, запись в outbox внутри транзакции надёжнее, чем попытка отправить сообщение отдельным действием после коммита.
Outbox не делает кеш мгновенно согласованным: между фиксацией транзакции и обработкой события проходит время. Обработчик должен переживать повторы, а событие обычно должно содержать идентификатор сущности и версию либо достаточно информации для безопасной повторной загрузки актуального состояния. Необработанные события и задержку доставки нужно наблюдать в проде.
PostgreSQL предлагает механизм уведомлений, но уведомление не заменяет долговечную очередь: слушатель может быть отключён и пропустить сигнал. Его можно использовать как подсказку обработчику проверить таблицу событий, но само событие для надёжной доставки следует хранить в базе.
Что ломается и как это проверить
- Инвалидируется не весь набор ключей. Одна запись может участвовать в кеше списка, счётчика и карточки. Составьте перечень представлений, зависящих от изменения, и проверьте их на одном тестовом сценарии.
- Ключ не учитывает контекст. Если результат зависит от языка, арендатора, валюты или прав доступа, эти параметры должны участвовать в ключе либо кешировать такой результат нельзя. Иначе данные одного пользователя могут попасть к другому.
- Шторм промахов. После массового истечения TTL множество запросов одновременно обращается к PostgreSQL за одним значением. Измеряйте число промахов и запросов к базе; для горячих ключей продумайте объединение одинаковых загрузок или постепенное обновление.
- Старый запрос заполняет кеш после инвалидации. Воспроизведите конкурентный сценарий: задержите чтение из базы, обновите запись и удалите ключ, затем завершите задержанный запрос. Если он возвращает старое значение из кеша, одного удаления недостаточно.
- Кеш недоступен, а приложение не имеет плана отказа. Решите заранее, может ли чтение временно идти напрямую в PostgreSQL, как ограничить дополнительную нагрузку и какие ошибки увидит клиент.
Перед запуском сравните систему с кешем и без него на одинаковом наборе запросов. Фиксируйте процент попаданий, задержку чтения, нагрузку на PostgreSQL, задержку обновления кеша и возраст возвращаемых данных. Универсальных цифр для допустимой устарелости нет: они зависят от того, что именно видит пользователь и какие последствия даёт старое значение.
Когда что выбирать
- Cache-aside и TTL — когда важны простота и снижение повторных чтений, а короткое окно устаревания приемлемо.
- Cache-aside с явной инвалидацией — когда изменения должны быстрее отражаться в чтении, а приложение контролирует все места записи.
- Write-through — когда задержка обновления кеша должна входить в ответ на запись и вы готовы обрабатывать частичный отказ двух хранилищ.
- Outbox — когда событие об инвалидации нельзя терять, а небольшая задержка доставки приемлема.
Не кешируйте без отдельной проверки данные, от которых зависят права доступа, остатки или финансовые операции: требования к допустимой устарелости там могут быть жёстче, чем выгода от ускорения. Инвалидация связанных представлений и согласованность между несколькими кеш-узлами — отдельные задачи; здесь рассматривается выбор базового пути чтения и записи.
Что делать дальше
Выберите один конкретный запрос и запишите три требования: сколько устаревания допустимо, сколько запросов кеш должен снять с PostgreSQL и что делать при недоступности кеша. Затем внедрите выбранную схему для одного типа данных, добавьте метрики попаданий, ошибок и задержки инвалидации и проверьте гонку «медленное чтение — обновление — заполнение кеша». Только после этого переносите стратегию на остальные данные.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: PostgreSQL 16: NOTIFY, Redis documentation: caching patterns
Частые вопросы
Cache-aside и write-through — это одно и то же?
Достаточно ли поставить TTL, чтобы данные всегда были актуальными?
Что произойдёт, если кеш упадёт после успешной записи в PostgreSQL?
Можно ли использовать PostgreSQL NOTIFY как очередь инвалидации?
Как не допустить, чтобы старый запрос вернул устаревший ключ после удаления?
Нужно ли кешировать каждый запрос к PostgreSQL?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.