🏗️ Раздел 17 · Вопрос #13

Что такое паттерн Database per Service

Ни один микросервис не имеет права напрямую подключаться к базе данных другого микросервиса, читать из её таблиц или выполнять SQL-запросы. Все взаимодействия между сервисами пр...


🟢 Junior Level

Database per Service (База данных на микросервис) — это базовый архитектурный паттерн микросервисной архитектуры, при котором каждый микросервис владеет своей собственной изолированной базой данных.

[Order Service] ──────► [(Order DB)]
      ▲                        │
      │ REST / gRPC / Kafka    │ (Прямой доступ к чужой БД СТРОГО ЗАПРЕЩЁН!)
      ▼                        ▼
[User Service]  ──────► [(User DB)]

Главный закон паттерна:

Ни один микросервис не имеет права напрямую подключаться к базе данных другого микросервиса, читать из её таблиц или выполнять SQL-запросы. Все взаимодействия между сервисами происходят только через публичный API (REST, gRPC) или события в брокере сообщений (Kafka, RabbitMQ).

Почему это необходимо:

  1. Слабая связанность (Loose Coupling): Команда сервиса User Service может изменить тип колонки phone или провести миграцию Liquibase/Flyway в любой момент без согласования с другими командами.
  2. Изоляция сбоев (Fault Tolerance): Если база данных каталога товаров упала из-за перегрузки, база данных оформления заказов и база платежей продолжают стабильно работать.
  3. Главная плата за удобство: В микросервисах невозможно сделать SQL JOIN между таблицами разных сервисов, а также теряются классические ACID-транзакции.

🟡 Middle Level

1. Три уровня физической изоляции данных

Уровень изоляции Архитектура Достоинства Недостатки / Риски
1. Database Server per Service У каждого микросервиса свой собственный кластер (например, отдельный AWS RDS / PostgreSQL). 🟢 100% изоляция CPU, RAM, дискового I/O и дисковых сбоев. Высокая финансовая стоимость; сложнее администрировать 50 кластеров.
2. Database Schema per Service Один общий инстанс СУБД, но у каждого сервиса своя схема (orders_schema.*, users_schema.*). 🟢 Дешевле в обслуживании; проще настраивать резервное копирование. 🔴 Эффект Noisy Neighbor: тяжелый запрос одного сервиса съедает весь CPU сервера БД.
3. Private Tables / Credentials Общая схема, но разные пользователи СУБД с жесткими GRANT только на свои таблицы. Самый низкий порог входа при распиле монолита. Риск случайного нарушения изоляции или утечки прав доступа.

2. Способы получения данных без SQL JOIN

А. API Composition (Синхронный сбор данных через BFF / Gateway)

Клиент запрашивает страницу заказа. API Gateway или специализированный BFF параллельно делает запросы:

  • GET /orders/101 $\to$ возвращает { id: 101, userId: 42, amount: 500 }
  • GET /users/42 $\to$ возвращает { id: 42, name: "Иван", email: "ivan@test.ru" } Шлюз объединяет два JSON-ответа в один экран для фронтенда.

Б. Event-Driven Data Replication (Асинхронная денормализация)

Сервис заказов подписывается на топик user-events. При создании или обновлении профиля пользователя Order Service сохраняет минимально необходимый снимок данных пользователя (userId, deliveryAddress, fullName) в свою локальную таблицу order_customers. Плюс: Order Service может мгновенно отобразить заказ локальным запросом без внешних сетевых вызовов.

В. CQRS (Command Query Responsibility Segregation)

Специальный Read-сервис слушает события от всех микросервисов системы и формирует денормализованные витрины данных в СУБД, оптимизированной под быстрый поиск и аналитику (Elasticsearch, ClickHouse, MongoDB).


🔴 Senior Level

1. Полиглотная персистентность (Polyglot Persistence)

Паттерн Database per Service освобождает от «золотого молотка» единой реляционной СУБД. Каждый ограниченный контекст (Bounded Context) выбирает тип базы данных под свой профиль нагрузки:

[User Service]      ──► PostgreSQL    (ACID-транзакции, строгая консистентность)
[Product Catalog]   ──► MongoDB       (Гибкая документная схема для категорий товаров)
[Shopping Cart]     ──► Redis         (Кэширование в оперативной памяти с TTL)
[Product Search]    ──► Elasticsearch (Полнотекстовый поиск, фасеты, Typo-Tolerance)
[Fraud Detection]   ──► Neo4j         (Графовая база данных для выявления сетей дропов)
[Analytics / BI]    ──► ClickHouse    (Колоночная СУБД для аналитики миллиардов событий)

2. Проблема согласованности и дрейфа данных (Data Drift)

При асинхронной денормализации данных (когда Order Service кэширует копию адреса пользователя) неизбежно возникает Data Drift (рассинхронизация данных):

  • Событие обновления адреса могло потеряться, застрять в Dead Letter Queue или быть обработано с ошибкой.
  • Приложение должно быть готово к Eventual Consistency (данные станут консистентными через доли секунды).
  • Методы борьбы:
    1. Audit / Reconciliation Jobs: Периодические ночные batch-процессы (или стриминговое сравнение через CDC/Flink), сверяющие контрольные суммы (hashes) данных между сервисами и публикующие корректирующие события.
    2. Data Ownership: Четкое разграничение ответственности: сервис пользователей является единственным источником истины (Single Source of Truth) для личных данных; остальные хранят только исторические слепки (Immutable Snapshot на момент оформления заказа).

4 Tricky Questions

1. Как реализовать удаление персональных данных пользователя по закону (GDPR / “Право на забвение”), если его ID и данные реплицированы в 20 независимых микросервисов?

Ответ:

  1. Choreographed / Orchestrated Deletion Saga: User Service публикует событие UserDeletedEvent(userId). Все 20 микросервисов обязаны подписаться на этот топик и выполнить очистку.
  2. Анонимизация вместо физического DELETE: Сервис заказов и сервис бухгалтерии по закону обязаны хранить финансовую отчетность 5 лет. Они не могут удалить строку заказа. Поэтому персональные данные заменяются на псевдоним: name = "GDPR_REMOVED", address = "ANONYMIZED", phone = null.
  3. Crypto-Shredding (Криптографическое уничтожение): Персональные данные хранятся зашифрованными индивидуальным ключом пользователя. При удалении пользователя его ключ уничтожается в централизованном хранилище ключей (KMS). После этого данные во всех 20 базах данных становятся нечитаемым шумом без необходимости их физического вычищения.

2. Как избежать проблемы $N+1$ сетевых запросов при синхронном API Composition (например, при отображении списка из 50 заказов с данными покупателей)?

Ответ: Если для списка из 50 заказов API Gateway сделает 1 запрос в Order Service и затем в цикле 50 запросов в User Service (GET /users/{id}), время ответа вырастет катастрофически. Решения:

  • Batch API: Реализация пакетного эндпоинта в User Service: POST /api/v1/users/batch-get с телом {"userIds": [1, 2, ... 50]}, возвращающего мапу пользователей за один сетевой вызов.
  • Локальная денормализация: При создании заказа сохранять имя и телефон покупателя прямо в документе заказа, полностью устраняя необходимость запроса к User Service при отображении списка.
  • GraphQL DataLoader: Использование механизма батчинга и дедупликации запросов на уровне GraphQL Gateway.

3. Что делать стартапу, если бизнес требует микросервисную архитектуру, но бюджет не позволяет оплачивать 30 независимых серверов баз данных?

Ответ: Использовать паттерн Logical Database per Service (Schema per Service):

  1. Развернуть один высоконадежный кластер PostgreSQL.
  2. Для каждого микросервиса создать отдельную логическую базу (CREATE DATABASE service_db) или схему (CREATE SCHEMA service_schema) с изолированным пользователем и паролем.
  3. Категорически запретить межсхемные запросы и JOIN на уровне прав доступа (REVOKE ALL ON ALL TABLES IN SCHEMA...).
  4. Внедрить ArchUnit тесты или SQL-линтеры в CI/CD, блокирующие обращение к чужим схемам. Когда компания вырастет, любую логическую схему можно вынести на отдельный физический сервер с помощью pg_dump без переписывания Java-кода.

4. Почему использование общих представлений (Shared Views) или процедур между базами сервисов является скрытым антипаттерном?

Ответ: Shared Views создают невидимую семантическую связанность на уровне схемы СУБД. При изменении внутренней структуры одной таблицы падает представление, о котором команда-разработчик могла даже не знать. Это ломает независимость релизных циклов и CI/CD пайплайнов, превращая микросервисы в худшую разновидность архитектуры — распределённый монолит (Distributed Monolith).


🎯 Шпаргалка для интервью

Главное:

  • Database per Service — золотое правило микросервисов: один сервис $\to$ одна собственная БД.
  • Никакого прямого доступа к чужой БД по SQL/JDBC. Доступ только через публичные контракты (API или брокер сообщений).
  • Плюсы: Независимость разработки, независимые миграции схем, изоляция отказов, возможность полиглотного хранения данных (Polyglot Persistence).
  • Минусы: Отсутствие межсервисных JOIN и ACID-транзакций, необходимость перехода на Eventual Consistency и паттерн Saga.
  • Получение чужих данных: API Composition (синхронно через Gateway/BFF) или Event-Driven денормализация (асинхронно через Kafka).
  • На ранних этапах допустим вариант Schema per Service на одном сервере СУБД с жестким разделением прав.

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Сервисы могут ходить в базы друг друга в режиме Read-Only для ускорения выборок» (Полностью ломает изоляцию и делает независимый деплой невозможным).
  • ❌ «Для объединения данных из разных сервисов мы используем распределенные SQL JOIN в коде» (Приведет к перекачиванию терабайтов данных в оперативную память приложения).
  • ❌ «Паттерн Database per Service требует покупки 50 отдельных физических серверов с первого дня» (Логическое разделение схем на одном сервере полностью сохраняет архитектурные границы при минимальном бюджете).

Связанные темы: