Содержание

Redis, RabbitMQ и S3-хранилище в Go-проектах

В backend-проектах часто появляются три инфраструктурных компонента: кэш, очередь и объектное хранилище. Они решают разные задачи и не заменяют друг друга.

  • Redis помогает быстро хранить временные данные.
  • RabbitMQ помогает выполнять задачи асинхронно.
  • S3 или MinIO помогают хранить файлы отдельно от базы данных.

Новички иногда пытаются использовать один инструмент для всего: хранить файлы в базе, делать очередь через Redis без понимания гарантий, складывать постоянные данные в кэш. Лучше сначала понять роль каждого компонента.

Redis как быстрый временный слой

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

Redis часто используют для:

  • кэша ответа API;
  • rate limit счетчиков;
  • session data;
  • short-lived locks;
  • временных токенов;
  • очередей простых фоновых задач.

Кэш нужен, когда вычисление или внешний запрос дорогие, а результат можно временно переиспользовать.

key := "weather:almaty" cached, err := redis.Get(ctx, key).Result() if err == nil { return cached, nil } value, err := fetchWeather(ctx) if err != nil { return "", err } if err := redis.Set(ctx, key, value, 10*time.Minute).Err(); err != nil { return "", fmt.Errorf("set cache: %w", err) } return value, nil

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

Инвалидация кэша

Кэш ускоряет чтение, но создает проблему актуальности. Если данные изменились, старый кэш может вернуть устаревший ответ.

Основные подходы:

  • короткий TTL;
  • удаление ключа при изменении данных;
  • versioned key: book:123:v2;
  • cache-aside;
  • write-through, когда запись идет и в основное хранилище, и в кэш.

Для учебных проектов чаще всего достаточно cache-aside и короткого TTL. Это проще и дает правильную привычку: кэш - это оптимизация, а не источник истины.

Redis для rate limiting

Rate limiting ограничивает частоту запросов. Redis удобен, потому что счетчик должен быть общим для всех экземпляров сервиса.

Идея:

rate:user:123:2026-05-23T10:15 -> 12

Ключ живет короткое время, например одну минуту. Каждый запрос увеличивает счетчик. Если счетчик выше лимита, API возвращает 429 Too Many Requests.

RabbitMQ как очередь

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

  • отправить email;
  • обработать изображение;
  • синхронизировать внешний API;
  • пересчитать отчет;
  • передать событие другому сервису;
  • выполнить долгую операцию вне HTTP-запроса.

Базовый поток:

producer -> exchange -> queue -> consumer

Producer публикует сообщение. Exchange решает, в какую очередь его отправить. Consumer читает сообщение и выполняет работу.

Пример события:

{ "event_id": "8f26f8a2-3c40-4a5f-9f17-6b2e3c2c2fb5", "type": "BookCreated", "book_id": "123", "created_at": "2026-05-23T12:00:00Z" }

Сообщение должно содержать достаточно данных для обработки, но не обязательно копировать всю базу.

Повторная доставка и idempotency

Очереди могут доставить сообщение повторно. Это не баг, а часть модели надежности. Consumer должен быть идемпотентным:

if alreadyProcessed(event.ID) { return nil } if err := process(event); err != nil { return err } markProcessed(event.ID)

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

Ack, retry и dead-letter

Consumer обычно подтверждает сообщение после успешной обработки. Это называется ack.

Если обработка упала, сообщение можно:

  • вернуть в очередь;
  • повторить позже;
  • отправить в dead-letter queue;
  • залогировать и остановить consumer, если ошибка критичная.

Dead-letter queue нужна для сообщений, которые не удается обработать после нескольких попыток. Иначе плохое сообщение может бесконечно крутиться в очереди.

S3 и MinIO

S3-совместимое хранилище используют для файлов:

  • аватары;
  • изображения;
  • архивы;
  • экспорты;
  • большие вложения;
  • сгенерированные отчеты.

MinIO удобен как локальная S3-совместимая среда для разработки. Код, который работает с MinIO, часто можно настроить на AWS S3 или другое S3-compatible хранилище.

Типичный поток загрузки:

  1. Проверить размер и тип файла.
  2. Сгенерировать безопасное имя.
  3. Загрузить объект в bucket.
  4. Сохранить метаданные в базе.
  5. Отдать пользователю URL или signed URL.

Что хранить в базе, а что в S3

В базе хранятся метаданные:

  • owner id;
  • bucket;
  • object key;
  • размер;
  • MIME type;
  • checksum, если нужен;
  • дата загрузки;
  • статус обработки.

В S3 хранится содержимое файла.

Не храните большие файлы прямо в PostgreSQL без сильной причины. База хороша для структурированных данных и транзакций. Объектное хранилище лучше подходит для больших бинарных объектов.

Безопасные имена файлов

Не доверяйте имени файла от пользователя:

../../../secret.txt my avatar.png

Обычно генерируют свой object key:

users/123/avatar/8f26f8a2.png

Оригинальное имя можно сохранить как метаданные, если оно нужно для отображения.

Signed URL

Если файл приватный, не делайте bucket публичным. Вместо этого выдавайте signed URL с ограниченным сроком действия:

https://storage.example.com/bucket/key?signature=...

Так пользователь получает временный доступ только к конкретному объекту.

Чеклист

  1. Redis-ключи имеют понятный namespace.
  2. У кэша есть TTL.
  3. Кэш не считается источником истины.
  4. RabbitMQ consumer идемпотентен.
  5. Сообщения имеют event_id.
  6. Для плохих сообщений есть retry и dead-letter подход.
  7. Файлы в S3 получают безопасные object keys.
  8. Metadata файла хранится в базе.
  9. Приватные файлы отдаются через signed URL.
  10. Локальная разработка поднимается через Docker Compose.

Следующий шаг после статьи

Закрепите тему в реальном проекте и продолжайте обучение на курсах Praxis.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи