Создание микросервисов — страница 25 из 69

Рис. 5.3. Ситуация после отказа от использования внешних ключей


Теперь уже понятно, что для составления отчета мы можем обойтись двумя вызовами, направляемыми к базе данных. И это правильно. То же самое произойдет при наличии двух отдельных сервисов. Обычно разговор о производительности при этом не идет. На это у меня есть довольно простой ответ: насколько быстрой должна быть ваша система? И насколько быстро она работает сейчас? Если есть возможность протестировать ее текущую производительность и разобраться в том, что значит высокая производительность, тогда можно почувствовать уверенность в правильности вносимых изменений. Иногда намеренно допускается замедление работы каких-либо компонентов, чтобы взамен получить какие-то другие преимущества, особенно если такое замедление вполне приемлемо.

А как же насчет взаимоотношений, обусловленных наличием внешних ключей? Мы их просто теряем. Теперь обязанность управления вменяется получающимся у нас сервисам и снимается с уровня базы данных. Это может означать, что нам придется постоянно проверять согласованность сервисов или предпринимать иные активные действия для очистки взаимосвязанных данных. Вопрос о необходимости таких действий зачастую не относится к выбору, осуществляемому технологом. Например, если прежний сервис содержал перечень идентификаторов для элементов каталога, то что произойдет, если элемент каталога удален и теперь заказ ссылается на неверный идентификатор каталога? Должны ли мы допускать подобную ситуацию? Если да, то как это должно быть представлено в заказе при выводе его на экран? Если нет, то как мы можем проверить отсутствие нарушений? На эти вопросы должны ответить те люди, которые определяют порядок поведения системы по отношению к ее пользователям.

Пример 2: совместно используемые статичные данные

Наверное, мне попадалось столько же много кодов стран в базах данных (рис. 5.4), сколько я написал классов StringUtils для собственных Java-проектов. Это позволяет предположить, что мы планируем вносить изменения в страны, поддерживаемые нашей системой, чаще, чем будет развертываться новый код, но какой бы ни была реальная причина, в этих примерах совместного использования статичных данных, хранящихся в базах данных, придумано много нового. Итак, что же нам предпринять для музыкального магазина, если все потенциальные сервисы считывают данные из одной и той же таблицы?

Рис. 5.4. Коды стран в базе данных


Итак, у нас есть несколько вариантов. Один из них предполагает дублирование этой таблицы для каждого из наших пакетов с тем, чтобы в долгосрочной перспективе она была продублирована также в каждом сервисе. Разумеется, это приводит к потенциальным осложнениям с согласованностью данных: что будет, если обновить одну таблицу с целью отображения создания некой страны Ньюмантопии на восточном побережье Австралии, оставив другие таблицы без изменений?

Второй вариант состоит в том, чтобы рассматривать совместно используемые статичные данные как код. Возможно, он мог бы содержаться в файле свойств, развернутом в виде части сервиса, или может использоваться в виде простого перечисления. Проблема с согласованностью данных остается, но опыт подсказывает, что намного проще поместить изменения в конфигурационные файлы, чем вносить их в действующие таблицы баз данных. Зачастую такой подход считается вполне разумным.

Третий, возможно, экстремальный вариант заключается в том, чтобы поместить статические данные в отдельный полноправный сервис. В двух ситуациях, с которыми мне приходилось сталкиваться, объема, сложности и количества правил, связанных со статическими ссылочными данными, было достаточно для того, чтобы считать такой подход оправданным, а вот когда дело касается просто кодов стран, он, вероятнее всего, будет излишним!

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

Пример 3: совместное использование данных

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

Рис. 5.5. Доступ к клиентским данным: мы ничего не упустили?


Как финансовый, так и складской код ведет запись и, возможно, время от времени осуществляет чтение из одной и той же таблицы. Как можно препарировать ее на части? Здесь мы имеем то, что вам будет попадаться довольно часто, — понятие области, не промоделированной в коде и фактически полностью смоделированной в базе данных. В этом случае пропущенным понятием области является Customer (Клиент).

Нам нужно превратить текущее абстрактное понятие клиента в конкретное. В качестве промежуточного этапа мы создаем новый пакет под названием Customer. Затем можно будет воспользоваться API для открытия кода Customer другим пакетам, например финансовому или складскому. Проделав все это, мы можем в итоге получить отдельный клиентский сервис (рис. 5.6).

Рис. 5.6. Распознавание ограниченного контекста клиента


Пример 4: совместно используемые таблицы

На рис. 5.7 показан последний пример. Каталогу нужно сохранять название и цену продаваемых музыкальных записей, а товарному складу — вести электронный учет материально-технических ресурсов. Мы решили содержать и то и другое в одном и том же месте — в универсальной таблице товарных позиций. Раньше, когда весь код составлял единое целое, нам не было понятно, что мы фактически объединяем интересы, но теперь можно увидеть, что действительно есть два различных понятия, которые должны сохраняться по-разному.

Рис. 5.7. Таблицы, совместно используемые различными контекстами


Ответ заключается в разбиении таблицы и получении двух таблиц (рис. 5.8), возможно, с созданием таблицы товарных позиций для склада и таблицы записей каталога для подробностей, необходимых сервису каталогов.

Рис. 5.8. Разбиение совместно используемой таблицы


Перестройка баз данных

То, что было рассмотрено в предыдущих примерах, относится к перестройкам баз данных, способствующих разделению ваших схем. Для более подробного изучения предмета можно обратиться к книге Скотта Дж. Амблера (Scott J. Ambler) и Прамода Садаладжа (Pramod J. Sadalage) Refactoring Databases (Addison-Wesley).

Поэтапное разбиение. Итак, уже найдены стыки в коде приложения, код сгруппирован вокруг ограниченных контекстов, все это использовано для нахождения стыков в базе данных и приложены все силы для ее разбиения. А что же дальше? Нужно ли выполнять радикальное разбиение, переходя от одного монолитного сервиса с единой схемой к двум сервисам, каждый из которых имеет собственную схему? Я настоятельно рекомендую разбить схему, но не разделять сервис до разбиения кода приложения на два отдельных микросервиса (рис. 5.9).

Рис. 5.9. Поэтапное разбиение сервиса


При отдельной схеме число потенциальных вызовов для выполнения одного действия будет потенциально увеличиваться. Там, где прежде можно было получать все нужные данные при выполнении одной инструкции SELECT, теперь придется извлекать данные из двух мест и объединять их в памяти. Кроме того, в результате перехода к двум схемам получается нарушение целостности транзакции, которое может существенно повлиять на наше приложение, о чем мы поговорим в следующем разделе. При разбиении схемы и неразбитом коде приложения мы оставляем для себя возможность вернуться к прежней схеме или продолжить настройки, никак не влияя на потребителей сервиса. Как только мы удостоверимся в том, что разделение базы данных имеет смысл, можно будет подумать и о разбиении кода приложения на два сервиса.

Транзакционные границы

Транзакции — вещь полезная. Они позволяют быть уверенным в том, что либо данные события произойдут вместе, либо не случится ни одно из них. Особую пользу они приобретают при вставке данных в базу, давая возможность одновременно обновлять сразу несколько таблиц и знать при этом, что в случае сбоя произойдет полный откат к прежнему состоянию, что исключит ситуацию, при которой данные будут находиться в несогласованном состоянии. Проще говоря, транзакции позволяют группировать вместе несколько различных действий, которые переносят нашу систему из одного согласованного состояния в другое, при этом либо все срабатывает, либо ничего не изменяется.

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

При монолитной схеме все операции по созданию или изменению, скорее всего, будут проводиться в рамках единой транзакционной границы. Когда мы разбиваем на части наши базы данных, то утрачиваем ту безопасность, которая обеспечивается при наличии единой транзакции. Рассмотрим простой пример в контексте MusicCorp. При создании заказа мне нужно обновить таблицу заказов, утвердив тем самым создание клиентского заказа, а также поместить запись в таблицу для команды товарного склада, чтобы оповестить ее о существовании заказа, который нужно скомплектовать для отгрузки. Мы добрались до распределения кода приложения на отдельные пакеты, в достаточной степени разделили клиентскую и складскую части схемы и приготовились поместить эти части в их собственные схемы, предваряя тем самым