Многие опытные разработчики недооценивают мощь инструментария БД при работе с микросервисами. По каким-то причинам в IT-сообществе приняты эмпирические правила - сервис маленький, значит и база маленькая. Но это не совсем так.
Скорее всего, при проектировании архитектуры у нас в голове чаще всего возникает паттерн database-per-service. И когда мы начинаем рисовать схемы, то возникает соблазн покрутить кирпичики модулей так, чтобы красиво их соединить стрелочками с цилиндриками, обозначающими БД.
С другой стороны, все знают, что много микросервисов, взаимодействующих через одну общую монструозную базу как Oracle, ElasticSearch и другие - это антипаттерн.
Поэтому выбирают маленькие(!) Postgres или MySQL.
Будем хранить немного данных, важных для конкретного микросервиса в своей небольшой БД, легко масштабироваться, да и вообще, красиво, быстро, по канонам.
А бизнес-логику вынесем в код.
Если загуглить, то первыми в выдаче будут статьи типа вот этой https://habr.com/ru/amp/publications/658151/
И картинка с маленькими DB

На самом деле, любая, даже самая компактная БД, особенно в production-среде имеет огромное количество преимуществ, которые упускаются из виду.

Далее я хочу рассмотреть реальные точки роста по производительности, удобству использования и иные преимущества, которые многие из нас упускают из виду при проектировании и реализации своих проектов.
Далее речь пойдет в большей своей части про реляционные БД на примерах Postgres.
Конечно, стоит здесь упомянуть и о таких базах, как ElasticSearch, ClickHouse, Oracle, Mongo. Они также используются в микросервисной архитектуре, но несколько для иных задач. В принципе, "тонкие" MongoDB тоже неплохо подходят для DB-per-service, но со своими фишками. Пару приемов из своего опыта в статье постараюсь привести.
Локальные преимущества
Когда мы разворачиваем БД локально, то чаще всего для скорости используем докер.
postgres:
image: postgres:latest
container_name: postgres
environment:
POSTGRES_PASSWORD: "postgres"
POSTGRES_MULTIPLE_DATABASES: users, products, orders, payments, experimentals
ports:
- "5432:5432"
volumes:
- ./multiple-databases.sh:/docker-entrypoint-initdb.d/multiple-databases.sh
deploy:
resources:
limits:
cpus: '1.50'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
command: >
postgres -c max_connections=1000
-c shared_buffers=256MB
-c effective_cache_size=768MB
-c maintenance_work_mem=64MB
-c checkpoint_completion_target=0.7
-c wal_buffers=16MB
-c default_statistics_target=100
Уже на этом этапе можно увидеть, что конфиг docker-compose.yml для секции с БД содержит гибкие настройки по ресурсам.
Согласитесь, что если внимательно оценить и спрогнозировать по нефункциональным требованиям, можно, как минимум, создать оптимальную по производительности и отказоустойчивости базу за 10-15 минут.
И такая база даже с минимально сконфигурированными ресурсами будет готова к работе - к вставке, чтению, триггерам, процедурам, математике, индексам и прочему, что реализовано в самом движке.
Таким образом, даже самая маленькая по весу данных база дает нам возможность крутить SQL-запросы с их фильтрацией, сортировками, группировками итд.
А мы вместо этого часто изобретаем велосипед в коде.
На Хабре регулярно выходят статьи из серии Hibernate ведет себя неадекватно.
Или ORM против чистого SQL
Знания чистого SQL - это не повод выкидывать категорично из своего арсенала возможностей и код. Просто надо понимать, когда и где их применять. Но об этом далее.
Пример использования: гибридный подход ORM + SQL
@Query("""
SELECT o FROM DomainOrganization do
INNER JOIN Organization o ON (do.organization = o AND do.domain.id = :id)
WHERE do.deletedAt IS NULL AND do.deletedBy IS NULL""")
fun findAllOrganizationsByDomainId(@Param("id") id: Long): List<Organization>
Здесь применены джоин с условиями мягкого удаления в SELECT запросе.
Конечно, можно написать и с фильтром. Вопрос - зачем? ?
fun findAllOrganizationsByDomainId(@Param("id") id: Long): List<Organization>
//findAllOrganizationsByDomainId(16).stream().filter(...)
Смысл рассуждений здесь такой - даже самая маленькая по конфигу БД запустилась, демон крутится, докер-контейнер не падает, значит минимальная работоспособность есть.
Тогда логично использовать ресурсы и движок БД на тех возможностях, которые нам даны, процесс не должен крутиться вхолостую и простаивать.
Тестирование
Продолжаем тему локального докера. В предыдущей статье я приводил пример генерации данных на лету через серии
SELECT generate_series(1, :weightAll/:weightNut ) AS num, 2 AS weight LIMIT :portionWeight
Когда нам нужно что-то быстро сгенерировать для тестов, чаще всего приходит на ум - создать таблицу, заполнить её инсертами, циклы из кода автотестов для вставки и чтения погонять. Да, это полезно в контексте тестирования, но тоже спрашивается - зачем? Чтобы потом это юзать при деплое через CI/CD? Ну тогда наверное полезно.
А для экспериментов на локалке? Значительно быстрее будет, зная где искать фишки, написать одну команду, никуда не переходя из редактора.
На моем предыдущем месте работы мы строили аналог диаграммы Ганта с графиком планирования по дням месяца, а далее по результатам собирали статистику и делали модуль аналитики.
Данные за день каждый раз обновлялись и сверялись с отдельной БД на Оракле, поэтому возникала потребность ... на год вперед по дням делать бесполезную вставку в новой таблице одной из схем. Хотя эти данные были нужны только в моменте, а потом никак не использовались. Короче была таблица на 1GB хлама, которая со временем росла.
Простейшее решение через генерацию серии в днях месяца легко решило вопрос.
SELECT generate_series(
'2025-01-01 00:00'::timestamp,
'2025-01-02 00:00'::timestamp,
'2 hours'
) AS times_data;
Кстати, этот трюк гораздо надежнее, чем может показаться. Иногда разработчики говорят - подумаешь, да мне проще руками написать. Ага, конечно. А потом эти наши LocalDateTime в папке Util живут как Legacy и периодически выстреливают себе в ногу в виде забытых if-else'ов при переходе через timezone.
Тема тестирования и отладки достаточно обширна и глубока, но суть остается похожей даже при локальной разработке. Если мы спроектировали БД не совсем удачно, что локализация через "юнит-тесты" на уровне базы (отдельно от кода) позволяет найти ошибку гораздо раньше, чем в интеграции.
Индексы
Прекрасный инструмент в использовании. Я уже писал выше, что выборка (удаление, другие функции) с условиями по грамотно построенным индексам ускоряется в разы по сравнению с фильтрацией на уровне кода, например, стримами. Или, что еще хуже, гонять циклами данные, использую оперативную память, как делают иногда на PHP.
А совсем плохо, когда
SELECT * FROM table
выбирается из БД в код бэкенда по запросу, не преобразуется никак и отдается на фронт в виде огромного массива бесполезных данных, которые героически разбираются на TS или JS за счет ресурсов клиента. Как говорил мой один коллега
Зачем данные туда-сюда гонять по сети
Просто создаем индексы и пользуемся
CREATE UNIQUE INDEX IF NOT EXISTS UX__domain_organization__domain_id_organization_id
ON domain_organization
USING btree (domain_id, organization_id) WHERE deleted_by IS NULL AND deleted_at IS NULL
Многие будут не согласны:
Индексы замедляют вставку и другие механизмы
Постойте, но у нас же микросервисная архитектура и маленькая по содержимому база. При грамотной реализации проблем вообще не возникнет, а если уж и возникнут - то используем реплику как CQRS-паттерне, ETL механизмы, очереди, есть масса вариантов.
Как раз локальное тестирование вам в этом поможет - мы не берем с потолка информацию, а в рамках нашего маленького докера все и проверим. И даже под нагрузкой.
Микросервис должен держать заявленные требования к производительности - должен.
Поэтому на уровне локального докера в рамках схемы и используем этот инструментарий, тут бояться нечего. Проблемы приходят позднее, и уж точно не из-за индексов.
А как же NoSQL?
Здесь принципы оптимизации через запрос прекрасно работают.
В одном проекте я делал модуль аналитики для менеджеров во фронтенд-кабинете.
До момента выполнения задачи функционал микросервиса был реализован так:
Сходить по разным индексам в ElasticSearch
Собрать через корутины необходимые данные
Погонять их в циклах, реализуя необходимую структуру в виде списков объектов
Отдать по REST на фронт через другой сервис
На фронте поманипулировать с результатами (округление, цвет, таблички, графики)
Выглядело это ужасно:
foreach
foreach
foreach
foreach
foreach
foreach
С точки зрения производительности еще хуже от 2-х до 10 секунд мог грузиться отчет, которых было несколько на странице.
Приняли решение делать вычисления на уровне движка Эластика.
Стало выглядеть еще ужаснее.
val resulQuery = getSearchQuery(queryBuilder, INTEREST_RATE_FIELDS_BG, 0)
.apply {
source()
//Блок agentReports
.aggregation(
AggregationBuilders.filters("f",
FiltersAggregator.KeyedFilter("agentReports" , bool {
must = listOf(
term {
"tenantId" to TenantEnum.AGENT.toString()
"bigGuarantee" to bigGuarantee.toString()
},
periodFromTo.buildRange("unformDateFilingGuarantee"),
)
}),
FiltersAggregator.KeyedFilter("toBankReports" , bool {
must = listOf(
terms { "tenantId" to bankTenants.orEmpty() },
periodFromTo.buildRange("unformDateApplicationGuarantee"),
term { "bigGuarantee" to bigGuarantee.toString() }
)
}),
FiltersAggregator.KeyedFilter("bankApproveReports" , bool {
must = listOf(
terms {"tenantId" to bankTenants.orEmpty() },
periodFromTo.buildRange("unformDateProposal"),
term { "bigGuarantee" to bigGuarantee.toString() }
)
}),
FiltersAggregator.KeyedFilter("bankIssuedReports" , bool {
must = listOf(
terms {"tenantId" to bankTenants.orEmpty() },
periodFromTo.buildRange("unformDateIssueGuarantee"),
term { "bigGuarantee" to bigGuarantee.toString() }
)
}),
)
.subAggregation(
AggregationBuilders.sum("guaranteeAmount").field("guaranteeAmount"),
).subAggregation(
AggregationBuilders.sum("commissionAmount").field("bankCommission")
).subAggregation(
AggregationBuilders.avg("guaranteeAmountAverage").field("guaranteeAmount")
)
)
}
val resultAggr = getAsync(resulQuery).aggregations
В результате вычисления ускорились, 100-300мс на ответ и группировку данных в сервисе.
Пояснение: ElasticSearch не обладает таким крутым синтаксисом команд, как SQL, чтобы что-то выбрать, требуется построить неплохой JSON.
А его трудно читать в сжатом виде, поэтому будет много переносов строк и табуляций.
По это схеме через паттерн Builder и создаются запросы из кода, как аналог ORM.
А принцип тот же - вычисления на уровне БАЗЫ одним ударом. Тем более, что базы были масштабированы и была реализована переливка данных по принципам ETL через Кафку.
Еще раз - если мы хотим роста производительности, то активно используем движки, а не гоняем данные вручную циклами.
Колоночные базы данных
В своем опыте я использовал ClickHouse. Хотя, эта база не предназначена для маленьких нужд, у него есть свои большие преимущества, такие как:
Легко ставится в докер
Относительно легко конфигурируется в маленьких проектах (пока не нужны реплики и разные движки)
Есть неплохая документация
Круто работает с большими данными
Есть бридж в движке к другим БД
SQL-подобный синтаксис
Это колоночная БД. Если супер-упрощенно, то схему хранения данных можно сравнить со списком папок и файлов в ОС, они примерно также лежат, только по колонкам, а не по строкам.
Это позволяет вставлять и хранить в CH огромные файлы, например, толстый CSV с 10-20 и более гигабайт. Причем, можно вставить так, что он сам достаточно быстро разберет его по колонкам, если пройдет валидацию.
Главное преимущество CH - огромная скорость вставки, по сравнению с другими СУБД.
Берете огромный массив данных и прямо в колонку массово пишете за считанные секунды/минуты.
А как же Кафка, очереди?
Достаточно часто бывают случаи, когда у вас проект не гигантский. Тогда оверинжениринг становится бессмысленным.
Базы данных вполне могут справляться с задачами, не требующими дополнительных мер.
Лет эдак 6 назад мне более опытный коллега показал прикольный трюк.
В том проекте у нас были небольшие по количеству в единицу времени платежи. Интегрировались с платежными системами и агрегаторами.
Фишка архитектуры была в том, что платеж как REST-запрос мог приходить в единственном (мало ли дубли) в единственный сервис без путаницы. Контроль же осуществлялся на стороне доверенного внешнего API. Поэтому как таковая транзакционная логика не была особо нужна. "Как бы чего не вышло" методы сервисов помечались как @Transactional, контроль был, тестирование проводили, ситуацию мониторили ,но вставка происходила в единственном экземпляре в нужную локализованную табличку.
Поэтому обновления статусов платежа были в целом надежными (аномалий не наблюдалось ни разу за 2-3 года).
Фокус заключался в следующем - делать вставку в таблицу Postgres, далее обновлять данные с приходом запросов с новым статусом и кидать по триггеру в отдельную табличку, которая разбиралась чуть позднее. И никаких очередей!
Триггеры в SQL-базах - отличный инструмент, когда нужна переливка. Еще есть также события events, но почему-то они почти не используются.
Всегда можно сделать триггер, отследить изменения, "отрезать" необходимый кусок данных и перелить куда нужно практически руками через свой собственный код.
В качестве примера ниже приведу код в развороте.
Дополнительный SQL-код для примера создания триггера
@SqlPatches([
SqlPatch("""CREATE INDEX IF NOT EXISTS IX__payment_transaction__account_id ON public.payment_transaction USING btree (account_id)"""),
SqlPatch("""CREATE INDEX IF NOT EXISTS IX__payment_transaction__organization_id ON public.payment_transaction USING btree (organization_id)"""),
SqlPatch("""CREATE INDEX IF NOT EXISTS IX__payment_transaction__external_id ON public.payment_transaction USING btree (external_id)"""),
SqlPatch("""CREATE INDEX IF NOT EXISTS IX__payment_transaction__order_id ON public.payment_transaction USING btree (order_id)"""),
SqlPatch("""CREATE INDEX IF NOT EXISTS IX__payment_transaction__payment_provider_configuration_id ON public.payment_transaction USING btree (payment_provider_configuration_id)"""),
SqlPatch("""CREATE INDEX IF NOT EXISTS IX__payment_transaction__state ON public.payment_transaction USING btree (state)"""),
/** SUCCESS state must have fact_amount */
SqlPatch("""ALTER TABLE payment_transaction
ADD CONSTRAINT CHK__payment_transaction CHECK (state != 'SUCCESS' OR fact_amount IS NOT NULL)"""),
SqlPatch("""ALTER TABLE public.payment_transaction ADD CONSTRAINT payment_transaction_un_order_id_ppc_id UNIQUE (order_id,payment_provider_configuration_id);
"""),
SqlPatch("""CREATE SEQUENCE IF NOT EXISTS payment_transaction_q_sequence START 1"""),
SqlPatch("""DROP TABLE IF EXISTS payment_transaction_q"""),
SqlPatch("""CREATE TABLE payment_transaction_q AS
SELECT
nextval('payment_transaction_q_sequence') as id,
created_at,
payment_provider_configuration_id,
account_id,
order_id,
external_id,
requested_amount,
fact_amount,
state,
test_transaction,
currency,
organization_id,
response_properties,
request_properties,
id as transaction_id
FROM payment_transaction WITH NO DATA"""),
SqlPatch("""ALTER TABLE payment_transaction_q ADD PRIMARY KEY (id)"""),
SqlPatch("""CREATE OR REPLACE FUNCTION payment_transaction_update() RETURNS TRIGGER AS ${'$'}${'$'}
DECLARE
created_at TIMESTAMP;
BEGIN
IF TG_OP = 'INSERT' OR (TG_OP = 'UPDATE' AND OLD.state != NEW.state) THEN
IF TG_OP = 'INSERT' THEN
created_at := NEW.created_at;
ELSE
created_at := NOW();
END IF;
INSERT INTO payment_transaction_q
SELECT
nextval('payment_transaction_q_sequence'),
created_at,
NEW.payment_provider_configuration_id,
NEW.account_id,
NEW.order_id,
NEW.external_id,
NEW.requested_amount,
NEW.fact_amount,
NEW.state,
NEW.test_transaction,
NEW.currency,
NEW.organization_id,
NEW.response_properties,
NEW.request_properties,
NEW.id;
END IF;
RETURN NULL;
END;
${'$'}${'$'} LANGUAGE plpgsql"""),
SqlPatch("""CREATE TRIGGER payment_transaction_trigger
AFTER INSERT OR UPDATE ON payment_transaction
FOR EACH ROW
EXECUTE PROCEDURE payment_transaction_update()""")
])
@TypeDefs(
TypeDef(name = "jsonb", typeClass = JsonBinaryType::class)
)
По поводу очередей в заключение этого раздела скажу свою позицию - они не нужны, пока
Нет требований
Работа 1 источник - 1 приемник
Нет нагрузки
Вкручивать в каждый проект старый подход не всегда разумно.
Едем дальше, будет еще интереснее.
Моя любимая тема - архитектура собственной БД
Чтобы что-то создать, надо что-то разрушить. (с) Дэвид. Фильм Ридли Скотта Прометей.
Что нужно разрушить? Людские стереотипы и предрассудки.
Многие думают, что БД (речь пойдет про SQL-СУБД) - это сложные джоины, функции, процедуры, нормальные формы, гибкость, "всё под рукой", "ой, Oracle это сложно" и другие варианты.
Или другая крайность - минимальная, легчайшая БД, а все остальное в коде.
Правда посередине - нет ничего сложного в БД, как и нет ничего легкого.
Сложность или легкость мы создаем (или разрушаем) сами!
И сейчас речь пойдет об элементарнейших вещах - о табличках.
Вернее не сейчас, чуть дальше.
Жизненный абсурдный пример.
Возьмем собственную квартиру. Как есть на данный момент. Зафиксировали.
Давайте выйдем на улицу и начнем собирать первый попавшийся хлам и нести домой. Типа понадобится. Мусор, бутылки, ветки, бумажки, всё, что найдем.
Рано или поздно наша квартира превратится в помойку, далее "увеличивая энтропию" мы загородим все проходу, в конечном итоге квартира забьется до упора мусором.
Как вы думаете, сложно или легко будет найти ваш паспорт в такой квартире? Вопрос риторический
Я клоню к тому, что при неверной организации данных БД можно довести до абсурдного состояния, когда всё работает ужасно или вообще не работает.
Почему? Потому что с самого-самого начала нужно думать о табличках. Просто о них.
Давайте разбираться.
Типы данных
При создании новой таблицы для колонок мы всегда выбираем тип - int, text, bool итд.
В этот прекрасный момент мы можем заложить себе бомбу замедленного действия, одну из:
Не тот тип
Зарезервировать лишнее
Зарезервировать мало
Создать неиспользуемое
В современном мире гонки за скоростью именно на этом этапе мы теряем самый важный ресурс - время.
Пиши в базу всё - потом разберемся, пусть все данные будут в БД
А что в итоге - через годы сервис лежит по причине избыточности. Масса денег и времени ушла вникуда. А как это поправить - переливки, репликации, костыли, убытки
А можно на создание таблицы с правильными типами потратить всего лишь 30-40 минут дополнительного времени. Чтобы потом не писать ALTERS
Таблицы и связи
Такая же ситуация. Просто нужно чуть больше времени уделить для требований.
Но не всё так плохо, могу подсказать рабочий лайфхак, который отлично справляется с нехваткой или переизбытком данных.
В SQL-базах давно существуют трюки с нормализацией и денормализацией.
Для этих целей придуманы 2 типа решений: через типы и через таблицы.
Про нормализацию написано огромное количество статей, на собеседованиях постоянно про неё спрашивают, в этом вопросе мы не заблудимся. А с денормолизацией как?
Очень просто - JSON, JSONB и подобные типы в помощь.
В микросервисах регулярно обмен данными происходит по REST в формате JSON.
Простейшим решением на Postgres тогда будет взять ВЕСЬ JSON или его часть от запроса ... и просто положить в нужную ячейку БД. Пусть будет. Остальные данные можно разобрать на уровне бизнес-логики и положить, куда требуется. А JSON в нужную ячейку.
Вы спросите - так это же ведь тоже будет хлам?
Нет. Мы забываем про переливку или партиции. Исторический слепок данных за предыдущий период (год, несколько лет) можно всегда держать в другом месте, не переполняя свою рабочую БД микросервиса.
И всё, никакого монолитного монстра. Как в обычной жизни - зимняя одежда в шкафу, летняя на вешалке. Или - хлам, который жалко выбрасывать отвезти в гараж или на дачу, вдруг пригодится.
Приведу простой пример - ФИО.
Лет 15 назад в мою бытность тестировщиком на одной из первых работ в карьере я увидел такие колонки в таблице:
first_name, last_name, middle_name, fio
Конечно же, это избыточно, fio - это простая конкатенация
Зачем её хранить в БД, можно просто на лету соединить в коде, или, что лучше, в запросе. А еще лучше во view
Или другой пример
Где достать данные, в базе в нужной табличке нет. Надо героически идти JOIN'ить или даже в коде через другой сервис.
А давайте начнем писать, это же просто сделать ALTER. На prod'e в огромной таблице.
В чем собственно лайфхак - экономия времени и мотивации на начальном этапе проектирования таблиц. Не надо стесняться взять 1-2 дня на продумывание архитектуры все вместе - с более опытными коллегами, с заказчиками, с аналитиками.
8-16 человеко-часов могут избавить нас от нагромождения лишнего, меньше потом писать кода, не делать костыльные микросервисы для решения проблем.
Решение через пример
Как использовать грамотно архитектуру таблиц в SQL БД. Введение:
На моем предыдущем месте работы по какой-то исторической причине в Postgres была табличка на 200+ столбцов.
В "лишних, но нужных" столбцах предполагалось хранить химические показатели примесей различных элементов и сплавов. Но использовались они в 1-2х местах один раз для небольшой математической формулы.
А для каждой строчки в таблице регулярно заполнялись избыточными null, нулями, прочерками итд.
Сам backend мы реализовывали на Spring Boot с забором и переливкой данных из Oracle для другого вычислительно модуля на Python'е.
В чем была проблема? В предрассудках, согласно которым мы сами себе ставили палки в колеса и тормозили не только производительность, но и разработку
Как решать подобные проблемы, когда не совсем понимаешь мифическую сложность задачи.
Во-первых нужно посмотреть код со стороны ... рутины.
При создании Data-класса или работе с объектом (получение изменение полей) мы сразу видим проблему - 200+ дополнительных строк. Да еще и не напрямую, а с getter'ами и setter'ами.
Всё, это сигнал - архитектура неверная. Действительно, поддержка даже 50 столбцов в табличке - это как минимум куча лишних строк кода.
Принцип - всё нужно здесь не работает. Мы путаемся от такого беспорядка.
Как решить проблему без гемора на prod'e?
Оценить локально или на тестовом стенде цену времени ALTER'а
Подготовить запрос
Сделать бэкап таблицы. Пусть это будут гигабайты или даже терабайты. Можно из под консольки фоново потренироваться как минимум, изучив масштабы.
Далее можно применить партицирование, хотя бы даже через LIMIT + OFFSET. Если не поможет - ну хотя бы скачать будет уже неплохо.
Далее постепенно отладить механизм.
И постараться залить заново в пустую, желательно в новую версию таблицы
Переключиться. Протестировать.
И сделать ALTER, положить это всё в JSON.
Как правило, нужно понимать, что масштабы бедствия пропорциональны объему данных, остальное - это механика движка БД. И чем раньше мы это сделаем, тем лучше.
Сделайте новое JSON поле, не убудет. Потом сделайте фиксы и начните работать с новым полем.
Для правильного создания колонок в базе, их трансформации и перепроектирования работает принцип - чем раньше, тем лучше.
Не стоит становиться в позу - потом, через код и прочее. Данные будут только прибывать.
Здесь от меня совет такой - надо научиться "чувствовать" данные, набраться терпения и опыта.
Последовательности
В Postgres есть такой инструмент как sequences.
Представим себе, что нам совсем не хочется использовать auto-increment 1,2,3... для наших сущностей. Например, в целях безопасности, чтобы нас было сложнее сканировать извне.
И uuid использовать совсем не хочется. Такое, промежуточное состояние. Здесь отлично в качестве инструмента подойдут последовательности.
Создайте общую nextval('your_sequence')
И используйте её для всех табличек, тогда инкремент будет "распределяться" по разным местам в Базе и итерироваться при вставках. Получим 1, 17,68,299 итд id-шники в одной таблице, а в другой будет уже, скажем, 2, 7, 89
JOIN's, транзакции, функции, процедуры и другие
Про эти инструменты знают все, статей в интернете предостаточное количество. Здесь подробно описывать не буду, лишь перечислю те вещи, на которые стоит обратить внимание.
JOIN'S нужно просто использовать. Набить, так сказать руку. Также не забываем про UNIONS и подзапросы.
Имена колонок - избегать хаотичных наименований, все создавать в едином стиле, как и имена таблиц.
Использовать схемы, если есть. В большинстве случаев запросы между схемами работают нормально, это просто имена, не более того.
Транзакции - надо знать. Всё равно рано или поздно выстрелят, лучше быть вооруженными и понимать, куда копать. Лучше избегать трёхэтажных транзакций.
IN Memory - прикольно, но дорого. Ошибки дешевле пофиксить, чем искать такие решения
Функции БД - легко гуглятся. Свои лучше не писать. Мы же в микросервисах работаем. Как и процедуры. Хотя это интересный вопрос - внимательные разработчики любят эти инструменты, но цена ошибки/опечатки высока.
Prod-данные. Хорошей практикой бывает смотреть на настоящие данные, а не только на тестовые. Закон Мёрфи.
Микросервисы со своими личными доступами. DB-per-service с репликацией и масштабированием - это разные сервисы. За это отвечают админы и DevOps's, но всё же. Новый сервис - новый логин, пароль, права.
И, самое главное - БАЗЫ ДАННЫХ, движки, модули создавались точно такими же разработчиками, как и мы. Поэтому, инструментарий там аналогичный, циклы, if-else итд.
А это значит, что полезно думать - как бы поступил разработчик БД, а не находиться в своем мире своей конкретной задачи, чтобы не изобретать велосипеды.
И, наконец, про Production
В микросервисной архитектуре есть свои договоренности по-умолчанию.
Сервисы как можно меньше
Горизонтальное масштабирование
Слабая связанность
Экономия на ресурсах
Если говорить простым языком, то сервис должен выполнять маленькие, укладывающиеся в выданные ему "железные" ресурсы. Поэтому при разработке стоит обращать внимание не только на функциональную задачу, но и на требования. Те самые "микро".
А иначе ваш сервис начинает расти и превращаться в монолит.
Поэтом неплохим решением здесь будет "сваливание" части функциональной ответственности на Базы Данных и на ожидаемые вменяемые задержки, паузы, которые неизбежно будут появляться со временем.
Даже простенький нормальный shared-хостинг как правило дает возможность крутить докеры на сервере за небольшие деньги + дает функции использования баз данных по полной.
А инструменты, такие как Яндекс Облако или Амазон так тем более.
Дело в том, что там работают люди опытные, знают статистику и изучают инциденты многие годы. Вследствие этого, они выставляют минимальный ресурсно-производительный порог для функционирования баз данных. Очень большую нагрузку на минималках конечно на такую базу навалить нельзя, рано или поздно ляжет, но так или иначе, свой средний уровень она должна держать.
Поэтому, перенос части бизнес-логики из приложения на уровень базы может буквально в разы повысить не только производительность систем ваших микросервисов, но и сэкономить неплохие деньги.
Т.е. если условно заполнить оперативку сервиса лишними данными (особенно неиспользуемыми), то её нужно будет расширять снова и снова. А это деньги. Тот самый бегунок в долларах.
А БД гораздо спокойнее относятся увеличению нагрузки с точки зрения механизмов, которые я описал в данной статье. Зачем делать по M*N лишней работы, если её можно сделать на уровне хранения, а не приложения.
И, напоследок
В процессе занятий музыкой я понял, как важен порядок. Элементарные операции с функционалом DAW или видео-редактора помогли мне осознать, насколько близко соприкасаются тематики IT и Sound Design.
Обычные, казалось бы вещи, такие как бэкапы. У меня случались неприятности, например, случайно безвозвратно удалил проект с песней, не сделав копию в облаке.
С БД тоже самое, если не озаботиться и понадеяться на админа/DevOps'а, то может произойти конфуз - недобросовестный девопс все дропнет и затребует деньги на восстановление как в одной недавней статье.
Еще один кейс - творческий бардак на рабочей машине. Куда же исчез мой проект? И начинаем судорожно искать по папкам и на гугл диске. Находим, только он называется не <Песня такая то> , а demo_part_1_metal.zip. Это аналогия к нормальному наименованию колонок и полей в БД и в коде.
Микросервисы - это тема очень интересная, огромное окно возможностей проявить в себя в оптимизации, в проектировании, в производительности, в гибкости. И Базы данных в этой сфере дают неплохой ресурс.
Anrol
Почему Postgres - это маленькая БД? Он масштабируется на любой объем данных, здесь вы кажется не правы. Это просто придирка, комментарий в другом:
База данных - это не только инструментарий и табличная алгебра (очень хоргошая кстати), но и куча других накладных вещей, в микросервисах лишних. Хотя тот же постгрес позволяет отключить WAL для таблиц, то есть не вести журналирование при модификации данных., он все же требует подключения, проверки прав, создание нового процесса. И, кстати, при небольших данных, наверное не более чем 8Кб, то есть одна data-page, постгрес не станет применять индекс, все сделает брутальным способом, построчным сравненим и т.п. и т.д.
И даже если индекс будет применяться, то на на таблице в 1000 строк при применении индекса postgres прочтет с диска 5 буферов (те же page), то есть 40Кб. А сколько вам собственно положено потратить на один запрос? для микросервиса норма 100 милисек. Поэтому конечно вы правы, что не надо бояться применять в микросервисах базы данных, но надо повозиться с конфигами и прочими сессионными переменными, создать RAM-диск и отдать его под tablespace если это целесообраз-но, посмотреть на планы запросов... и в итоге может даже так случиться, что проще не лезть в БД, а применить известные алгоритмы прямо в коде вашего микросервиса.