DBAtlas · Атлас эксплуатации баз данных

Эталонная архитектура: уровень данных микросервисов

В микросервисах самое важное решение — владение данными: каждый сервис должен владеть своими данными (database-per-service) и не обращаться напрямую к таблицам другого сервиса. Это дерево пошагово рассматривает антипаттерн общей БД, достижение согласованности между сервисами без распределённых транзакций (saga + outbox), синхронизацию данных через CDC и межсервисные запросы (API composition vs модель чтения CQRS). Цель — сохранить сервисы действительно независимыми и раздельно развёртываемыми.

Как сервисы владеют данными: общая БД или по одной на сервис?

Изоляция данных — основа микросервисной архитектуры. Сервисы используют одну общую базу данных (скрытая зависимость), у каждого сервиса своя база данных (правильный паттерн), или реальная проблема — согласованность/запросы между сервисами? Сначала определите текущее состояние.

  • Все сервисы используют одну общую БД
  • У каждого сервиса своя БД, но операциям нужна согласованность между сервисами
  • У каждого сервиса своя БД, но нужны объединённые запросы/отчёты

Как обеспечить согласованность межсервисного бизнес-процесса?

Для операции, охватывающей несколько сервисов (создать заказ → принять оплату → списать остаток), классическая распределённая транзакция (2PC) хрупка и не масштабируется. Вместо этого разбейте процесс на шаги «локальная транзакция + событие» (saga) и надёжно публикуйте события (outbox). С какой проблемой вы имеете дело?

  • Я пытаюсь сделать многосервисный процесс атомарным (2PC?)
  • Не удаётся согласовать локальную запись и публикацию события (dual write)

Как вы будете обслуживать запрос, показывающий данные из нескольких сервисов?

Когда данные разделены между сервисами, общий экран невозможно построить одним join. Есть два основных подхода: вызывать API соответствующих сервисов во время запроса и объединять результаты (API composition) или поддерживать заранее объединённую модель чтения, наполняемую событиями (CQRS). Выбор определяют частота и сложность запросов.

  • Простое, редкое объединение; достаточно вызвать несколько сервисов и объединить
  • Сложный/частый объединённый запрос; нужна готовая модель чтения

Рекомендация: database-per-service (разделите общую БД)

Совместное использование одной базы данных сервисами — скрытая и опасная зависимость: изменение схемы одного сервиса ломает другие, владение таблицами становится неясным, а независимое развёртывание — невозможным. Каждый сервис должен владеть своими данными.

Рекомендация: паттерн saga (избегайте распределённых транзакций)

Не пытайтесь сделать операцию, охватывающую несколько сервисов, атомарной одной распределённой транзакцией (2PC); 2PC удерживает блокировки и приносит хрупкость единой точки и низкую доступность. Вместо этого разбейте процесс на шаги, каждый из которых — локальная транзакция в своём сервисе, и отменяйте их компенсирующими шагами при сбое.

Рекомендация: Transactional outbox (ловушка двойной записи)

Когда сервису нужно и записать данные в свою БД, и опубликовать событие, выполнение этих действий по отдельности — ловушка «двойной записи» (dual write): если одно удалось, а другое нет, система остаётся в несогласованном состоянии. Решение — записать событие в таблицу outbox в ТОЙ ЖЕ локальной транзакции, что и бизнес-данные, а затем публиковать его отдельным процессом.

Рекомендация: API composition (объединение во время запроса)

Там, где нужны объединённые данные, вызывающая сторона (например, gateway/BFF) обращается к API соответствующих сервисов и объединяет результаты в памяти. Это самое простое решение для несложных соединений, охватывающих немного сервисов; дополнительная копия данных не хранится.

Рекомендация: модель чтения CQRS (наполняется событиями)

Для сложных и частых объединённых запросов держите заранее соединённую, оптимизированную под запросы модель чтения (материализованное представление/денормализованная копия), наполняемую событиями от сервисов. Сторона записи остаётся в сервисах; сторона чтения быстро отдаёт готовые данные из одного места.

Открыть в DBAtlas →