Прямой ответ
Владение данными означает, что у каждой сущности есть один сервис, который её создаёт и изменяет. Все остальные получают данные через контракт владельца — запрос или событие — и работают с собственной копией.
Правило простое: одна сущность — один владелец. Если два сервиса пишут в одну таблицу, владение не определено, и это проявляется при первом конфликте.
Три вопроса, на которые нужно ответить для каждой сущности:
- Кто создаёт запись? Сервис, в сценарии которого сущность появляется.
- Кто изменяет поля? Только владелец; остальные запрашивают изменение через его API.
- Кто читает и как? Через API владельца либо через локальную копию, обновляемую событиями.
Типовое распределение в магазине:
| Сущность | Владелец | Почему |
|---|---|---|
| Товар | Каталог | Создаётся и описывается в каталоге |
| Остаток | Склад | Меняется операциями склада |
| Цена | Ценообразование | Зависит от акций и себестоимости |
| Заказ | Заказы | Создаётся в сценарии оформления |
| Платёж | Оплата | Создаётся в сценарии оплаты |
Ключевой момент: «товар» не принадлежит одному сервису целиком. Каталог владеет описанием, склад — остатком, ценообразование — ценой. Попытка собрать всё в одну таблицу и отдать одному сервису создаёт точку согласования всех изменений.
Данные у потребителя хранятся как локальная копия, построенная по событиям владельца:
class CatalogProjection:
"""Копия данных каталога внутри сервиса заказов."""
def on_product_updated(self, event: dict) -> None:
self.db.execute(
"""INSERT INTO catalog_cache (product_id, title, sku)
VALUES (%(id)s, %(title)s, %(sku)s)
ON CONFLICT (product_id)
DO UPDATE SET title = EXCLUDED.title, sku = EXCLUDED.sku""",
event,
)
Копия содержит только те поля, которые нужны потребителю. Это не дублирование ради дублирования, а обмен сетевой задержки на локальное чтение.
Цена такого решения — задержка согласованности. Копия обновляется асинхронно, поэтому между событием и его применением проходит время. Для большинства сценариев это допустимо; о моделях согласованности — в статье про eventual consistency.
Практика: как определять и защищать владение
1. Владелец определяется по сценарию создания, а не по тому, кто чаще читает. Сервис доставки читает товары постоянно, но владельцем каталога не становится.
2. Запись только через владельца. Если сервису нужно изменить чужие данные, он отправляет команду владельцу, а не пишет в его таблицу. Отказ прав на уровне базы делает нарушение невозможным — см. статью про общую базу.
3. Одно поле — один владелец. Спор о поле решается вопросом «какой сценарий его меняет». Если два сценария меняют одно поле, это признак того, что в модели смешаны разные понятия.
4. Локальная копия — не источник истины. Её нельзя использовать для принятия решений о записи. Решение принимается владельцем, копия служит для чтения.
5. Версионируйте события. Потребители обновляются в разное время, поэтому событие должно сохранять обратную совместимость; о версионировании — в статье про версии API.
6. Проверяйте целостность копий. Периодическая сверка копии с источником выявляет пропущенные события. Альтернативный источник изменений — CDC, читающий журнал базы; о механизме — в статье про CDC.
7. Документируйте владельцев. Список сущностей и их владельцев должен лежать в репозитории. Без записи владение определяется тем, кто первым написал в таблицу.
Рекомендации:
- Начинайте с таблиц, а не с диаграмм. Присвойте владельца каждой таблице — это конкретное действие, которое сразу выявляет конфликты.
- Отзовите права на запись у невладельцев. Это самая быстрая и самая показательная мера.
- Не создавайте копии без необходимости. Если поле читается редко, запрос к владельцу дешевле, чем поддержка проекции.
- Измеряйте задержку обновления копии. Метрика возраста данных показывает, когда eventual consistency перестаёт быть приемлемой.
- Разделяйте команду и запрос. Модель, оптимизированная для чтения, может отличаться от модели записи — о подходе в статье про CQRS.
Практический сценарий: два сервиса по-разному считали статус заказа
Контекст: сервис доставки показывал заказ как доставленный, а сервис поддержки — как в пути. Пользователи жаловались на противоречивую информацию.
- Диагностика. Оба сервиса хранили поле статуса и изменяли его самостоятельно: доставка ставила «доставлен» по факту курьера, поддержка — по обращению клиента.
- Причина. Владение статусом не было определено, поэтому каждый сервис считал своё значение верным. Общая таблица позволяла писать обоим.
- Уточнение модели. Выяснилось, что смешаны два разных понятия: состояние логистики и состояние обращения. Это разные сущности с разными владельцами.
- Действия. (а) Владельцем статуса заказа назначили сервис заказов; (б) доставка и поддержка получили собственные поля — «статус доставки» и «статус обращения» — в своих схемах; (в) запись в чужие таблицы закрыли отзывом прав; (г) статус заказа обновлялся командой владельцу, а потребители получали событие; (д) добавили сверку возраста данных с алертом при задержке больше минуты.
- Результат. Противоречия исчезли, поскольку каждое поле имеет одного владельца; задержка обновления копии видна в метрике и не превышает нескольких секунд.
Вывод: расхождение данных между сервисами почти всегда означает неопределённое владение. Решение — не синхронизация значений, а разделение понятий и назначение владельца каждому из них.
FAQ
Что делать, если поле нужно двум сервисам для записи? Разделить понятие. Обычно за общим именем скрываются два разных состояния, каждое со своим владельцем и своим жизненным циклом.
Можно ли читать чужие данные напрямую из базы? Нет, это создаёт зависимость от чужой схемы. Чтение выполняется через API владельца либо через локальную копию.
Как обеспечить атомарность изменения в двух сервисах? Через сагу с компенсирующими действиями: единой транзакции между базами не существует; о паттерне — в статье про saga и outbox.
Нужен ли отдельный сервис-справочник для общих данных? Только если данные действительно общие и имеют отдельный жизненный цикл. Чаще «общий справочник» становится точкой согласования всех изменений и тормозит команды.
Источники: microservices.io (database per service, event sourcing, CQRS) — ссылки в шапке.