Архитектура и данные

Когда проекту действительно нужен Redis, а когда PostgreSQL вполне достаточно

У нового веб-проекта появляется первая архитектурная схема:

Nginx
  ↓
Application
  ↓
PostgreSQL

Через некоторое время кто-то предлагает:

Нужно добавить Redis.

На вопрос:

Зачем?

обычно звучит:

Он быстрый.

Или:

Для кеша.

Или:

В нормальном production Redis должен быть.

После этого архитектура превращается в:

Nginx
   ↓
Application
   ↓
┌────────────┬─────────┐
│            │         │
PostgreSQL  Redis    Worker

Теперь нужно:

устанавливать Redis;
защищать его;
мониторить;
обновлять;
проверять память;
решать вопросы persistence;
обрабатывать его недоступность;
синхронизировать кеш с PostgreSQL.

При этом приложение может обслуживать 500 пользователей, выполнять несколько десятков запросов в секунду и прекрасно работать только на PostgreSQL.

Получается странная ситуация:

Redis добавили раньше, чем появилась проблема, которую он должен решить.

Но существует и противоположная ошибка.

Проект вырос.

Одни и те же тяжёлые данные запрашиваются тысячи раз в секунду.

Rate limit постоянно пишет в PostgreSQL.

Сессии создают лишнюю нагрузку.

Realtime-события нужно быстро раздавать нескольким экземплярам API.

А команда продолжает:

PostgreSQL ведь всё может.

Технически — многое.

Архитектурно — не каждую задачу стоит заставлять решать одним инструментом.

Поэтому правильный вопрос звучит не:

«Что быстрее — Redis или PostgreSQL?»

А:

«Какое состояние мы храним, насколько долго оно должно жить и какие гарантии нам от него нужны?»

Именно с этого и стоит начинать выбор.


PostgreSQL и Redis решают разные задачи

PostgreSQL — прежде всего реляционная система управления данными.

Она хорошо подходит для хранения:

пользователей;
проектов;
заказов;
платежей;
ролей;
сообщений;
истории;
связей между сущностями.

То есть данных, которые являются источником истины продукта.

Если PostgreSQL потерять, продукт потеряет бизнес-состояние.


Redis устроен иначе.

Он особенно удобен для быстро доступных структур данных, которым часто нужны:

очень низкая задержка;
TTL;
счётчики;
быстрые атомарные операции;
временное состояние;
pub/sub;
stream processing;
cache.

Официальная документация Redis прямо выделяет среди типичных сценариев:

cache;
session storage;
rate limiting;
job queues;
pub/sub;
streams;
leaderboards.

То есть сравнивать их только по принципу:

Redis быстрее
PostgreSQL медленнее

не очень полезно.

У них разная роль.


Первый вопрос: является ли значение бизнес-данными или производным состоянием?

Представим SaaS.

Есть таблица:

projects

с проектом:

id = 1842
status = IN_PROGRESS
owner = 52

Это бизнес-факт.

Его нельзя потерять только потому, что сервер Redis перезапустился.

Источник истины здесь очевиден:

PostgreSQL.

Теперь другой пример.

Для проекта нужно показать:

Количество непрочитанных уведомлений:
17

Это значение можно вычислить:

SELECT COUNT(*)
FROM notifications
WHERE user_id = 52
  AND read_at IS NULL;

Если таких запросов очень много, результат можно временно кешировать:

notifications:unread:52
→ 17
TTL 30 sec

Даже если Redis потеряет этот ключ:

ничего необратимого не произошло.

Значение можно получить из PostgreSQL снова.

Вот это уже хороший кандидат для кеша.


Очень полезное правило

Перед тем как положить данные в Redis, спросите:

Если прямо сейчас удалить весь Redis, сможем ли мы восстановить это значение?

Если:

да, из PostgreSQL или другого источника

перед нами хороший кандидат на ephemeral/cache state.

Если:

нет, данные исчезнут навсегда

нужно очень внимательно подумать, действительно ли Redis должен быть единственным их хранилищем.


Но PostgreSQL тоже использует память

Иногда Redis добавляют из рассуждения:

PostgreSQL каждый раз читает данные с диска, а Redis — из RAM.

Это слишком упрощённая модель.

PostgreSQL имеет собственный buffer cache, а операционная система также кеширует файловые данные. В документации PostgreSQL shared_buffers как раз управляет объёмом памяти, выделенным под shared buffer cache; EXPLAIN умеет отдельно показывать buffer hits, когда физическое чтение блока удалось избежать благодаря кешу.

Поэтому хорошо индексированный запрос вроде:

SELECT id, name
FROM users
WHERE id = 1842;

не обязательно означает постоянное физическое чтение SSD.


Redis не должен маскировать плохой SQL

Представим страницу:

/projects

открывается за:

2,8 секунды.

Команда решает:

Добавим Redis.

После кеширования:

50 ms.

Кажется, проблема решена.

Но EXPLAIN ANALYZE показывает:

Seq Scan on projects

по таблице из нескольких миллионов записей.

А запрос постоянно фильтрует:

WHERE organization_id = $1
ORDER BY created_at DESC;

и нужного индекса просто нет.


Правильное первое действие

Создать подходящий индекс, например концептуально:

CREATE INDEX
ON projects (
    organization_id,
    created_at DESC
);

После этого запрос может стать достаточно быстрым без Redis.


Почему это важно

Redis в такой ситуации не исправляет query model.

Он добавляет ещё одну проблему:

Когда инвалидировать cache?

Например проект изменился.

PostgreSQL уже содержит:

status = COMPLETED

Redis всё ещё:

status = IN_PROGRESS

Теперь приложение имеет два состояния одного факта.


Cache invalidation — реальная архитектурная стоимость

Простой cache-aside выглядит так:

Request
   ↓
Redis?
 ┌─┴─┐
Yes  No
 │    ↓
 │ PostgreSQL
 │    ↓
 │ Redis SET
 └────┘
   ↓
Response

Красиво.

Но после:

UPDATE project

возникает вопрос:

Что делать с cache?

Вариант 1 — удалить кеш после записи

UPDATE PostgreSQL
↓
DEL project:1842

Следующий запрос заново заполнит cache.


Но что если PostgreSQL обновился, а Redis недоступен?

Получаем:

DB:
COMPLETED

Cache:
IN_PROGRESS

Поэтому кеш должен проектироваться так, чтобы потеря или ошибка Redis не разрушала корректность бизнес-данных.


Это одна из причин использовать TTL

Например:

project:1842
TTL = 30 sec

Даже если invalidation потерялся, stale-состояние ограничено по времени.

Но допустимость этих 30 секунд определяется продуктом.


Для каталога товаров 30 секунд могут быть нормальными

Например:

описание;
изображение;
категория.

Для остатка последней единицы товара — уже нет

Если Redis говорит:

stock = 1

а PostgreSQL:

stock = 0

на основании cache нельзя разрешать продажу.

Здесь authoritative business decision должен опираться на источник истины и корректную конкурентную транзакцию.


Поэтому кешировать чтение и принимать бизнес-решение — не одно и то же

Redis может помочь быстро показать:

примерное количество;
каталог;
публичный профиль;
настройки, редко меняющиеся.

Но критичные операции:

списать деньги;
уменьшить stock;
выдать доступ;
подтвердить заказ.

должны иметь чёткую authoritative модель.


Когда PostgreSQL вполне достаточно для кешируемого на первый взгляд запроса

Например dashboard показывает:

12 проектов
5 активных
3 ожидают оплаты

Таблица содержит:

5 000 строк.

Запрос занимает:

8 ms.

И выполняется:

несколько раз в минуту.

Redis здесь, скорее всего, ничего существенного не улучшит.


Он добавит

новое соединение;
новый service;
новые failure modes;
cache invalidation;
мониторинг.

ради экономии нескольких миллисекунд.

Это сомнительный обмен.


Что делать, если dashboard действительно тяжёлый

Допустим агрегат:

SELECT ...
FROM orders
JOIN payments
JOIN users
...

по миллионам строк занимает секунды.

Redis — один вариант.

Но не единственный.

PostgreSQL имеет materialized views, которые физически сохраняют результат запроса и позволяют обновлять его через REFRESH MATERIALIZED VIEW. Это особенно удобно для статистики и dashboard, которым допустима контролируемая задержка обновления.


Например

Вместо:

каждый пользовательский request
↓
огромный aggregate

можно:

background refresh
↓
sales_summary
↓
быстрый SELECT

Иногда Redis после этого вообще не требуется.


Значит первый принцип такой

Не добавляйте cache до того, как поняли стоимость исходного запроса.

Сначала:

EXPLAIN ANALYZE;
indexes;
query shape;
pagination;
N+1;
materialized/read model.

И только потом:

Redis.

Теперь рассмотрим sessions

Здесь Redis становится намного интереснее.

Представим один backend:

Nginx
↓
API #1

Можно хранить session в PostgreSQL:

sessions

id
user_id
expires_at
data

Для небольшого продукта это совершенно нормальная архитектура.


Пользователь входит

Session ID
↓
PostgreSQL
↓
User

При нормальных индексах и умеренной нагрузке проблема может вообще никогда не появиться.


Зачем тогда Redis для sessions?

Например появляется:

API #1
API #2
API #3

Все instance должны очень быстро получать session state.

Сессии:

часто читаются;
имеют TTL;
временные по природе;

То есть очень хорошо соответствуют модели Redis.

Официальная документация Redis отдельно описывает session storage с автоматическим EXPIRE, включая sliding TTL и управление несколькими сессиями одного пользователя.


Но Redis для session всё равно не обязателен

Можно продолжать использовать:

PostgreSQL sessions.

Особенно если:

QPS небольшой;
PostgreSQL далеко не перегружен;
простота инфраструктуры важнее микросекунд.

Или использовать stateless-токены, если их свойства подходят конкретной модели безопасности.


То есть правило снова то же

Не:

Sessions → обязательно Redis.

А:

Какие требования у наших sessions?

Следующая популярная задача — rate limiting

Например API разрешает:

100 запросов в минуту
на пользователя.

Нужно вести счётчик:

rate:user:1842

с ограниченным сроком жизни.

Это почти идеальная модель для Redis:

counter
+
atomic increment
+
TTL.

Redis и сам официально выделяет rate limiting как один из основных прикладных сценариев.


Можно ли сделать rate limit на PostgreSQL?

Конечно.

Например:

api_rate_limits

user_id
window
count

и атомарно обновлять строку.

Для:

нескольких запросов в секунду

это вполне может работать.


Проблема появляется при высоком потоке

Например public API получает:

50 000 requests/sec.

Постоянные:

INSERT/UPDATE

счётчиков создают бессмысленную для основного business database write-нагрузку.

Значения при этом живут:

60 секунд.

И после этого больше не нужны.


Это типичный пример данных, которым не обязательно жить в PostgreSQL

Именно потому, что они:

короткоживущие;
очень часто изменяются;
легко пересоздаются;
не являются бизнес-историей.

Redis здесь выглядит значительно естественнее.


Следующий сценарий — счётчики

Например:

online users;
просмотры;
количество действий в минуту;
temporary counters.

PostgreSQL способен выполнять:

UPDATE counters
SET value = value + 1
WHERE key = '...';

Но если один hot counter обновляется десятки тысяч раз в секунду, он превращается в точку contention.

Redis с атомарными counter operations может быть гораздо удобнее.


Но вопрос снова в масштабе

Если счётчик меняется:

20 раз в минуту,

Redis добавлять ради него странно.


Онлайн-пользователи — хороший пример ephemeral-state

Пользователь отправляет heartbeat.

Мы хотим знать:

Кто был активен последние 2 минуты?

Такое состояние:

быстро устаревает;
не представляет исторической ценности;
имеет естественный TTL.

Redis подходит очень хорошо.


А вот историю посещений уже лучше хранить иначе

Если бизнес хочет анализировать:

кто заходил;
когда;
сколько сессий;

это уже исторические данные.

Их можно агрегировать/сохранять в PostgreSQL, аналитической системе или другом долговременном storage.

Один и тот же пользовательский факт может иметь:

операционное краткоживущее представление

и:

долговременную историю.

Теперь background jobs

Это одна из самых интересных областей.

Часто звучит:

У нас worker, значит нужен Redis.

Это неверно.


Очередь можно построить на PostgreSQL

Например:

jobs

id
type
payload
status
available_at
attempts

Worker выбирает задачу:

SELECT ...
FROM jobs
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;

SKIP LOCKED позволяет нескольким workers не ждать строки, уже захваченные другим процессом; PostgreSQL прямо показывает этот подход для снижения lock contention в конкурентной обработке строк.


Получается архитектура

API
 ↓
PostgreSQL jobs
 ↓
Worker #1
Worker #2
Worker #3

И отдельный Redis вообще не нужен.


У PostgreSQL-очереди есть сильное преимущество

Представим бизнес-операцию:

создать заказ
+
создать job SEND_ORDER_EMAIL

Можно выполнить:

BEGIN

INSERT order
INSERT job

COMMIT

Теперь либо существуют:

и Order
и Job,

либо:

ни одного.

Очень полезное свойство.


Если очередь находится в отдельном Redis

Получаем:

BEGIN PostgreSQL
INSERT order
COMMIT

↓
Redis enqueue

Что если:

PostgreSQL succeeded
Redis failed?

Заказ существует.

Job отсутствует.

Теперь нужен:

outbox;
retry;
reconciliation

или другой механизм согласования двух систем.


Поэтому для небольшого и среднего приложения PostgreSQL queue может быть очень сильным решением

Особенно если:

задач немного;
они тесно связаны с business transaction;
PostgreSQL уже является обязательной зависимостью.

Мы уменьшаем количество moving parts.


Когда Redis queue становится интереснее

Например поток jobs очень большой.

Нужны специфические инструменты/библиотеки вокруг Redis.

Нужны:

очень быстрые transient queues;
consumer coordination;
специфичная модель обработки.

Redis официально рассматривает job queues как отдельный use case, а Redis Streams предоставляют consumer groups, acknowledgements, pending entries и повторное назначение необработанных сообщений.


Но Redis Pub/Sub и надёжная очередь — не одно и то же

Это очень важное различие.

Представим:

Publisher
↓
Redis Pub/Sub
↓
Subscriber

Subscriber в момент сообщения отключён.

Для обычной pub/sub-модели событие может быть просто потеряно.


Поэтому Pub/Sub хорош для другого класса событий

Например:

обновить UI;
сообщить WebSocket-instance;
инвалидировать локальный кеш;
разослать transient realtime signal.

Где история не является обязательной.


Для durable jobs нужны другие механизмы

Например:

PostgreSQL jobs;
Redis Streams;
специализированная очередь.

То есть:

Redis

сам по себе ещё не означает:

надёжная очередь.

Нужно определить используемую структуру и семантику доставки.


PostgreSQL тоже умеет уведомлять процессы

Есть:

LISTEN

и:

NOTIFY

PostgreSQL позволяет клиентским процессам подписываться на channels и получать уведомления при NOTIFY. Документация прямо описывает это как простой механизм межпроцессного взаимодействия для приложений, работающих с одной БД.


Например PostgreSQL queue можно сделать гибридной

Durable job находится:

jobs table.

После commit:

NOTIFY jobs;

Worker не обязан постоянно выполнять:

SELECT
SELECT
SELECT

Он получает сигнал:

Появилась работа.

Но источником истины остаётся таблица.


Это важное различие

NOTIFY

может служить сигналом.

jobs table

— durable state.

Даже если worker пропустил сигнал, job остаётся в таблице и будет найдена позже.


Для многих небольших SaaS этого более чем достаточно

Архитектура:

Application
  ↓
PostgreSQL
  ├── business data
  ├── jobs
  └── NOTIFY

Worker

очень проста в эксплуатации.


А как быть с distributed locks?

Представим scheduler запущен сразу в двух API instance.

Оба хотят выполнить:

daily report.

Нужно обеспечить:

только один исполнитель.

Часто первая мысль:

Redis lock.

Но PostgreSQL уже имеет advisory locks.

Официальная документация PostgreSQL предоставляет session-level и transaction-level advisory locks для application-defined ресурсов.


Например концептуально

try acquire lock:
daily-report-2026-09-29

Получил один process.

Второй:

lock unavailable
→ skip.

Если приложение уже зависит от PostgreSQL, это может быть значительно проще добавления Redis только ради locks.


Когда Redis-lock всё-таки оправдан

Например координируются:

несколько независимых сервисов,

не имеющих общей PostgreSQL.

И Redis уже является общей инфраструктурой.

Но distributed locking требует очень аккуратного определения:

TTL;
ownership;
failure semantics;
что произойдёт после network partition.

Нельзя считать:

SET if not exists

универсальной заменой продуманной модели конкурентности.


Особенно опасно использовать lock вместо database constraint

Например правило:

Один активный invoice на операцию.

Можно попытаться:

Redis lock
↓
check
↓
insert

Но последней защитой всё равно полезно иметь:

UNIQUE constraint

в PostgreSQL.

Почему?

Redis может быть недоступен.

Lock может истечь.

Процесс может зависнуть.

А бизнес-инвариант должен оставаться истинным.


Поэтому concurrency и caching не должны подменять database invariants

Redis:

ускоряет;
координирует;
временно хранит.

PostgreSQL:

защищает durable business state.

У них разные обязанности.


Теперь realtime

Представим приложение имеет:

API #1
API #2

Пользователь A подключён по WebSocket к:

API #1.

Администратор отправил сообщение через:

API #2.

Как API #1 узнает:

Нужно отправить событие пользователю A?

На одном instance всё просто

sendToSocket(userId)

Но при нескольких instances нужен межпроцессный канал.


Redis Pub/Sub здесь очень удобен

Например:

API #2
↓
publish user:1842
↓
Redis
↓
API #1
↓
WebSocket

Если сообщение уже сохранено в PostgreSQL, Redis здесь отвечает только за оперативную доставку realtime-сигнала.


Если Pub/Sub-сигнал потерялся, данные не потеряны

Пользователь обновляет страницу.

API читает:

message

из PostgreSQL.

То есть:

PostgreSQL
→ durable truth

Redis Pub/Sub
→ realtime acceleration.

Это очень здоровая архитектурная граница.


Такой подход лучше, чем хранить единственную копию сообщения в Pub/Sub

Потому что realtime transport и история сообщений имеют разные требования.


Redis становится особенно ценным, когда экземпляров приложения много

Например:

API #1
API #2
API #3
API #4
API #5

Нужны:

shared sessions;
pub/sub;
rate limit;
temporary coordination;
cache.

Один Redis способен закрыть несколько действительно существующих задач.

Теперь его эксплуатационная стоимость уже начинает окупаться.


Это намного сильнее аргумента

Redis — стандартный элемент стека.

Когда PostgreSQL обычно достаточно

Представим обычную CRM:

200 пользователей;
1–2 API instance;
несколько workers;
умеренная нагрузка.

Есть:

клиенты;
проекты;
сообщения;
файлы;
уведомления;
админка.

PostgreSQL вполне может хранить:

business data;
sessions;
jobs;
counters;
locks.

Задачи можно обрабатывать через PostgreSQL queue

Например:

email;
cleanup;
publication;
notification;
storage deletion.

Locks — advisory locks или row locks

Сессии — обычная таблица с:

expires_at.

Dashboard — индексы или:

materialized view.

Realtime-сигналы при небольшой архитектуре:

LISTEN / NOTIFY

или даже локальный event bus, если API один.


Добавлять Redis в такую систему можно

Но вопрос:

Что именно станет лучше?

Если нет убедительного ответа, возможно, пока не нужно.


Пример №1. Небольшой B2B-сервис

Архитектура:

Nginx
↓
Node.js
↓
PostgreSQL
↓
Worker

Нагрузка:

20 requests/sec peak.

Jobs:

500/day.

Sessions:

300 active.

Добавлять Redis просто ради:

sessions + queue

вряд ли обязательно.

PostgreSQL справится без труда при нормальной схеме.


Пример №2. SaaS быстро вырос

Теперь:

API instances = 8
active sessions = 80 000
requests = thousands/sec

Есть:

rate limiting;
realtime;
hot public cache;
temporary counters.

Redis становится значительно интереснее.

Не потому, что:

пользователей стало много.

А потому что появился конкретный набор workloads, для которых memory-oriented shared state подходит лучше.


Пример №3. Каталог с большим read traffic

PostgreSQL хранит:

Product
Price
Category

Публичная страница одного популярного товара получает:

20 000 requests/min.

Товар меняется:

раз в несколько часов.

Это хороший кандидат на cache

product:1842
TTL = 60 sec

Количество обращений к PostgreSQL резко уменьшается.

Staleness в пределах минуты допустима.

Redis приносит понятную измеримую пользу.


Пример №4. Финансовый баланс

Есть:

account.balance

Кто-то предлагает:

Давайте хранить баланс в Redis — быстрее.

Здесь нужно быть намного осторожнее.

Баланс — критичный бизнес-факт.

Он требует:

transactionality;
audit;
durability;
concurrency rules.

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

Для обычного SaaS PostgreSQL выглядит естественнее как authoritative storage.


Redis при этом может хранить производное представление

Например:

balance display cache.

Но операция:

можно ли списать 10 000?

должна опираться на authoritative финансовую модель.


Пример №5. Rate limit формы регистрации

Нагрузка:

5 регистраций в минуту.

PostgreSQL спокойно справится.

Можно даже реализовать ограничение на application level и хранить события.


Через год public API получает миллионы запросов

Теперь rate-limit counters:

живут 1 минуту;
меняются постоянно;
не нужны в истории.

Redis начинает быть почти идеальным инструментом.


Значит решение зависит не только от функции

Одна и та же функция:

rate limit

при одном масштабе может спокойно жить на PostgreSQL.

При другом — Redis даёт существенную пользу.


Необходимо измерять, а не угадывать

Перед добавлением Redis полезно ответить:

Какой QPS?
Какая p95/p99 latency?
Какие SQL-запросы самые дорогие?
Есть ли lock contention?
Сколько чтений повторяют одни данные?
Какие значения имеют естественный TTL?

Без этих цифр архитектурное решение строится на ощущениях.


Один из самых плохих аргументов

PostgreSQL уже загружен на 40%, давайте ставить Redis.

Почему именно 40%?

Что загружено?

CPU?
Disk I/O?
connections?
locks?
slow queries?

Redis может вообще не влиять на проблемный ресурс.


Например bottleneck — плохой INSERT

Если система упирается в массовую запись:

audit events

кеш SELECT ничего не изменит.

Если bottleneck:

connection pool,

Redis-cache может немного уменьшить SQL traffic, но настоящая причина всё ещё останется.


Сначала нужно локализовать проблему

Типичная последовательность:

Metrics
↓
Slow queries
↓
EXPLAIN ANALYZE
↓
Indexes
↓
Query/model optimization
↓
Load test
↓
только затем cache

Так Redis становится решением измеренной проблемы.


Есть ещё in-process cache

Если API пока один:

API #1

и нужно кешировать небольшой immutable/reference dataset, можно использовать память самого приложения.

Например:

countries;
static configuration;
feature metadata.

Нет смысла поднимать network cache для данных:

10 KB

только потому, что слово cache ассоциируется с Redis.


Но in-memory cache перестаёт быть общим при нескольких instances

Появилось:

API #1
API #2
API #3

У каждого свой cache.

Теперь invalidation становится сложнее.

Здесь Redis может дать общий shared cache.


Ещё один риск Redis — скрытая зависимость

Сначала Redis используется:

только для кеша.

То есть приложение умеет работать без него.

Потом туда добавляют:

sessions.

Потом:

rate limit.

Потом:

queue.

Потом:

locks.

Через год Redis становится:

критической точкой отказа.

Хотя архитектурно команда продолжает считать:

Это просто кеш.

Поэтому для Redis тоже нужен dependency classification

Например:

Cache
→ degradation acceptable
Sessions
→ authentication unavailable
Queue
→ background processing stops

Один Redis instance может обслуживать несколько use cases, но последствия его потери у них разные.


Это важно для readiness

Если Redis используется только как cache:

Redis DOWN

может означать:

Ready = true
Degraded = true

Приложение просто идёт в PostgreSQL.


Если Redis хранит sessions

Redis DOWN

означает:

пользователи не могут нормально работать.

Readiness и monitoring уже должны относиться к Redis как к критичной dependency.


Нельзя одновременно говорить

Redis неважен, это кеш.

и использовать его как:

единственное хранилище пользовательских сессий;
единственную очередь критичных jobs;
единственный источник temporary auth state.

Архитектура должна честно отражать реальную роль компонента.


Cache stampede — ещё одна проблема

Представим популярный ключ:

catalog:homepage

TTL истекает.

Одновременно приходят:

5 000 requests.

Каждый видит:

cache miss

и идёт в PostgreSQL.

Мы получили:

5 000 тяжёлых запросов

вместо одного.

Это cache stampede.


Значит качественный кеш — больше, чем GET/SET

Могут потребоваться:

jitter TTL;
locking/single-flight;
stale-while-revalidate;
background refresh.

Redis уменьшает часть нагрузки, но сам вводит архитектурные решения.


Ещё одна проблема — слишком длинный TTL

Например:

price cache:
24 hours.

Бизнес изменил цену.

Пользователь ещё сутки видит старую.


Слишком короткий TTL тоже может быть бессмысленным

TTL = 1 sec

при:

10 requests/minute.

Практически каждый запрос снова идёт в PostgreSQL.

Cache существует, а выгоды почти нет.


TTL должен следовать бизнес-требованию к свежести

Не:

Обычно ставим 5 минут.

А:

Сколько stale data допустимо для этой функции?

Cache hit ratio нужно измерять

Например:

Cache hits:
95%

может означать существенную пользу.

А:

hits:
8%

при сложной invalidation-системе заставляет спросить:

Нужен ли вообще этот cache?

Redis не должен становиться «корзиной для всего временного»

Очень легко начать складывать:

temporary state;
unfinished forms;
payment state;
one-time tokens;
background jobs;
user settings.

потому что:

Redis уже есть.

Но каждый тип данных должен иметь собственные требования к:

durability;
TTL;
recovery;
audit.

Техническая доступность хранилища не означает архитектурную пригодность.


Например password reset token

Можно хранить:

в PostgreSQL

с:

hash;
expires_at;
used_at.

Плюсы:

audit;
одна транзакционная система;
простая диагностика.

Можно использовать Redis, если модель продукта и нагрузка это оправдывают.

Но само наличие TTL ещё не делает Redis обязательным.


Именно это отличает архитектурный выбор от шаблона

Плохой шаблон:

sessions → Redis
jobs → Redis
cache → Redis
locks → Redis

Хорошая архитектура спрашивает для каждого случая:

Нужна durability?

Нужен TTL?

Какой throughput?

Что при потере данных?

Нужна транзакция с business data?

Нужна история?

Нужны JOIN/поиск?

Какой failure mode?

И уже потом выбирает storage.


Сравним PostgreSQL и Redis в типичных задачах

ЗадачаPostgreSQLRedis
Основные бизнес-данныеОтличноОбычно не первый выбор
Relational dataОтличноНе основная специализация
Транзакции между сущностямиСильная сторонаДругая модель
CacheВозможно, но не отдельный cache layerСильная сторона
TTL-состояниеМожно реализоватьОчень удобно
SessionsПодходитОчень удобно при высоком трафике
Rate limitingПодходит при умеренной нагрузкеСильная сторона
Hot countersМожет стать contention pointОчень удобно
Job queueОтлично для многих системТоже сильный вариант
Pub/SubLISTEN/NOTIFY для простых задачСильная сторона
Durable historyОтличноТребует отдельного проектирования
Complex queries/JOINСильная сторонаНе для этого
Advisory locksЕстьЕсть свои coordination-паттерны
LeaderboardsМожно реализоватьSorted sets очень удобны
Temporary shared stateМожноОчень естественный use case

Практический выбор для нового веб-продукта

Для нового CRM или SaaS мы бы чаще начинали:

Application
+
PostgreSQL

Если нужны background jobs:

Application
+
PostgreSQL
+
Worker

И только после появления конкретной необходимости добавляли:

Redis.

Почему такой порядок полезен

Потому что первая версия продукта обычно имеет больше неопределённости в бизнесе, чем проблем производительности.

Нам важнее быстро изменять:

workflow;
permissions;
billing;
projects;
CRM logic.

чем заранее строить сложный cache layer.


Кроме того, Redis всегда можно добавить позже

В большинстве случаев переход:

PostgreSQL-only
↓
PostgreSQL + Redis cache

не требует переписывать всю domain model.

Особенно если application layer уже отделяет:

источник истины

от:

ускоряющего слоя.

А удалить Redis позднее может оказаться сложнее

Если вокруг него уже построены:

sessions;
queue;
locks;
pub/sub;
cache;
rate limit,

компонент становится глубоко встроен в инфраструктуру.

Поэтому добавлять его лучше сознательно.


Удобная эволюция может выглядеть так

Этап 1

Nginx
↓
Application
↓
PostgreSQL

Этап 2

Появились фоновые задачи:

Application
↓
PostgreSQL jobs
↓
Worker

Этап 3

Появились несколько API instances и realtime:

Application × N
↓
PostgreSQL

Redis
→ Pub/Sub

Этап 4

Read traffic вырос:

Redis
→ cache

Этап 5

Rate limiting стал высокочастотным:

Redis
→ rate counters

Redis появился не потому, что:

архитектура должна выглядеть профессионально.

А потому что продукт постепенно создал несколько задач, для которых он действительно полезен.


Когда мы бы точно задумались о Redis

Например:

одни и те же дорогие данные
читаются тысячи раз;
нужен общий cache
между множеством API;
есть high-frequency rate limiting;
нужны большое количество
короткоживущих counters;
нужен быстрый shared session store;
несколько realtime instances
должны обмениваться transient events;
требования очереди хорошо соответствуют
Redis Streams/экосистеме.

Это конкретные причины.


Когда PostgreSQL пока достаточно

Например:

нагрузка умеренная;
SQL после индексирования быстрый;
один или два API;
сессий немного;
jobs тесно связаны с business transactions;
rate limit не создаёт заметной write load;
realtime прост или отсутствует.

В этой ситуации второй stateful service может быть преждевременной оптимизацией.


Самый полезный вопрос перед установкой Redis

Мы бы попросили закончить фразу:

«Без Redis сейчас у нас существует проблема ________, которую мы измерили как ________. Redis решит её за счёт ________.»

Например:

Без Redis 85% запросов публичного каталога повторно выполняют один тяжёлый SQL. На пике это 4 000 одинаковых чтений в секунду. Redis cache-aside с TTL 60 секунд уменьшит количество обращений к PostgreSQL.

Это хороший архитектурный аргумент.


А вот плохой

Redis нужен, потому что он быстрый.

Что именно мы ускоряем?

До какой величины?

Какая текущая проблема?

Что произойдёт при его отказе?

Если ответа нет, компонент пока добавляется ради архитектурного ощущения.


Redis должен иметь собственный failure plan

Допустим мы его всё-таки добавили.

Нужно заранее определить:

Что если Redis недоступен?

Для cache:

fallback → PostgreSQL.

Для sessions:

auth degraded/unavailable.

Для rate limit:

fail-open или fail-closed?

Для queue:

как восстанавливаются pending jobs?

Для pub/sub:

можно ли потерять событие?

Это пять разных ответов.


Особенно интересен rate limit

Если Redis упал, можно:

fail-open

то есть временно пропускать запросы.

Плюсы:

основной сервис доступен.

Минусы:

защита ослаблена.

Или:

fail-closed

то есть блокировать запросы.

Плюсы:

лимит нельзя обойти.

Минусы:

отказ Redis
останавливает функцию.

Правильный выбор зависит от того, что защищается.


Это показывает важную вещь

Redis — это не просто ускоритель.

Как только продукт начинает зависеть от него, Redis становится частью business availability model.

Его нужно включать в:

health-check;
readiness;
monitoring;
alerts;
capacity planning.

Если Redis используется как cache, readiness может не зависеть от него

Например:

PostgreSQL ✓
Redis      ✗

Получаем:

Ready:
YES

Status:
DEGRADED

Если Redis хранит обязательные sessions

Та же ситуация:

PostgreSQL ✓
Redis      ✗

может дать:

Ready:
NO

или частичную деградацию конкретных routes.

Таким образом даже health-check определяется архитектурным смыслом Redis, а не названием технологии.


Capacity planning тоже отличается

PostgreSQL в основном планируется вокруг:

disk;
RAM;
connections;
I/O;
query workload.

Redis — особенно вокруг:

RAM;
key count;
TTL;
eviction policy;
traffic.

Если команда не знает:

Что произойдёт, когда Redis заполнит память?

архитектура ещё не закончена.


Cache eviction не должна удалять единственную копию важных данных

Очевидное правило.

Но его легко нарушить постепенно.

Сначала:

cache only.

Потом туда кладут что-то, чего в PostgreSQL уже нет.

Теперь eviction начинает означать потерю данных.

Поэтому источник истины должен быть определён явно.


Как мы бы организовали архитектуру

Например:

                    Application
                     /       \
                    /         \
                   ▼           ▼
            PostgreSQL       Redis
                 │             │
                 │             ├── cache
                 │             ├── sessions
                 │             ├── rate limit
                 │             └── pub/sub
                 │
                 ├── users
                 ├── projects
                 ├── payments
                 ├── files metadata
                 └── durable jobs

В этой схеме понятно:

PostgreSQL
→ authoritative persistent state
Redis
→ specialised fast/ephemeral state

Но другая система может выглядеть иначе

Например Redis Streams используется как значимый message infrastructure.

Тогда:

Redis

уже не просто optional cache.

Он становится полноценной критичной подсистемой.

Это нормально.

Главное — не притворяться, что архитектурная роль осталась прежней.


Redis не является обязательным этапом взросления продукта

Нет такой последовательности:

SQLite
↓
PostgreSQL
↓
Redis
↓
Kafka
↓
Kubernetes

как уровней компьютерной игры.

Каждая технология появляется только когда её свойства соответствуют новой задаче.

Можно построить серьёзный веб-продукт:

PostgreSQL
+
Workers
+
Object Storage

и годами не нуждаться в Redis.


И можно нуждаться в Redis уже в небольшом продукте

Например продукт сам по себе — realtime leaderboard.

Даже при небольшой команде его основная задача отлично соответствует:

Redis Sorted Sets.

То есть размер компании тоже не является универсальным критерием.


Выбор определяется workload

Именно это ключевой вывод.


Практический checklist перед добавлением Redis

Мы бы проверили:

  • существует ли измеримая проблема, а не гипотетическая будущая нагрузка;
  • оптимизированы ли SQL и индексы;
  • проверен ли EXPLAIN ANALYZE дорогих запросов;
  • можно ли решить задачу PostgreSQL materialized/read model;
  • действительно ли данные имеют короткий lifecycle или естественный TTL;
  • можно ли безопасно потерять Redis-состояние;
  • известен ли authoritative source данных;
  • определено ли поведение при cache miss;
  • определено ли поведение при недоступности Redis;
  • понятна ли стратегия cache invalidation;
  • измеряется ли cache hit ratio;
  • не используется ли Redis для маскировки N+1 или отсутствующего индекса;
  • нужна ли одна транзакция между business data и job;
  • подойдет ли PostgreSQL queue через FOR UPDATE SKIP LOCKED;
  • можно ли решить coordination через PostgreSQL advisory locks;
  • нужен ли именно durable stream или достаточно transient Pub/Sub;
  • существует ли реальный scale, при котором PostgreSQL rate counters стали проблемой;
  • готовы ли monitoring/readiness учитывать новый компонент;
  • известно ли, что произойдёт при исчерпании Redis memory;
  • оправдывает ли полученная выгода дополнительный stateful service.

Если на большинство вопросов пока нет ответа, возможно, Redis добавляется слишком рано.


Самая практичная стратегия — PostgreSQL first, Redis when justified

Это не означает:

Всегда избегайте Redis.

Напротив.

Redis — чрезвычайно полезный инструмент.

Но особенно хорошо он работает тогда, когда получает узкую и понятную ответственность.

Например:

PostgreSQL
→ business truth

Redis
→ hot cache

или:

PostgreSQL
→ users

Redis
→ high-frequency rate limit.

или:

PostgreSQL
→ messages history

Redis Pub/Sub
→ realtime delivery.

Граница сразу понятна.


Значительно хуже выглядит

PostgreSQL:
часть данных

Redis:
ещё какая-то часть данных

а что является источником истины —
зависит от endpoint.

Так постепенно появляется система, в которой любое расхождение превращается в сложное расследование.


Вместо вывода

Redis действительно способен сделать веб-продукт быстрее и удобнее масштабировать.

Он отлично подходит для:

cache;
sessions;
rate limiting;
hot counters;
pub/sub;
streams;
некоторых типов queues;
короткоживущего shared state.

Но из этого не следует:

Любому production-приложению нужен Redis.

PostgreSQL уже способен решать значительно больше задач, чем иногда предполагают.

Он может хранить:

sessions;
jobs;
counters;
locks;
notifications;
materialized data.

Для очередей можно использовать конкурентное чтение через FOR UPDATE SKIP LOCKED.

Для application-level coordination существуют advisory locks.

Для простых межпроцессных сигналов — LISTEN/NOTIFY.

Для тяжёлых read-моделей — индексы, оптимизированные запросы и materialized views.

И всё это происходит внутри одной уже существующей транзакционной системы.

Поэтому для нового CRM, SaaS или B2B-продукта хорошая стартовая схема часто остаётся очень простой:

Application
↓
PostgreSQL
↓
Worker

А Redis появляется позже — в тот момент, когда можно закончить фразу:

«Мы добавляем Redis, потому что вот эта конкретная нагрузка или временное состояние уже стало проблемой, и Redis решает именно её».

Тогда Redis перестаёт быть обязательной строчкой на архитектурной диаграмме.

Он становится тем, чем и должна быть любая инфраструктурная технология:

инструментом для решения измеримой задачи.

Есть похожая задача?

Опишите продукт, интеграции и ограничения. До разработки зафиксируем объём, риски и критерии приёмки.

Обсудить проект →