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

СУБД является неотъемлемой частью хоть сколько‑нибудь серьезного современного приложения. Соответственно, при проектировании приложения может возникнуть вопрос, как лучше сервисам взаимодействовать с базой данных: предоставляя общий доступ к одной базе или же у каждого микросервиса должна быть своя база данных. Мы рассмотрим два шаблона, предназначенных для решения данной задачи — это Shared database и Database per Microservice. Начнем с Shared database.

Общая база

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

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

При этом сама логика работы запросов может быть различной. Иногда запросам требуется объединять данные, принадлежащие нескольким сервисам. Например, для поиска клиентов в определенном регионе и их последних заказов требуется объединение клиентов и заказов.

Но в микросервисной архитектуре, в соответствии с идеологией, сервисы должны быть слабо связаны, чтобы их можно было разрабатывать, развертывать и масштабировать независимо друг от друга.

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

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

Посмотрим, как можно реализовать Shared DB паттерн на практике. Сервисы TicketService и UserService имеют доступ к таблицам друг друга. Например, TicketService можно использовать следующую транзакцию ACID, чтобы гарантировать, что новый заказ не нарушит кредитный лимит клиента:

BEGIN TRANSACTION

…

SELECT TICKET_TOTAL

 FROM TICKETS WHERE USER_ID = ?

…

SELECT CREDIT_LIMIT

FROM USERS WHERE USER_ID = ?

…

INSERT INTO TICKETS …

…

COMMIT TRANSACTION

Как и у всех других паттернов, здесь также есть свои преимущества и недостатки, и соответственно, ситуации, когда предпочтительнее их использовать.

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

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

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

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

В качестве альтернативы шаблону Shared DB выступает Database per service.

Каждому сервису по базе

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

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

Основными преимуществами данного шаблона является слабая связанность сервисов. Изменения в базе данных, используемой одним сервисом не влияют на другие сервисы как в случае с шаблоном Shared Database.

Так наши сервисы могут использовать те СУБД, которые для них лучше всего подходят. Например один сервис может использовать Elastic поиск, второй NoSQL, третий SQL, если этого требует бизнес логика.

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

Также у нас возникают сложности с управлением несколькими базами данных в нашем приложении. Например, если используются PostgreSQL, NoSQL и Elastic, то нам потребуются специалисты, знакомые со всеми этими БД.

Заключение

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

Больше актуальных навыков по архитектуре приложений вы можете получить в рамках практических онлайн‑курсов от экспертов отрасли.

Комментарии (9)


  1. Akina
    02.10.2024 04:59
    +2

    Например, TicketService можно использовать следующую транзакцию ACID, чтобы гарантировать, что новый заказ не нарушит кредитный лимит клиента

    Вот постоянно удивляет это желание создать себе проблему, дабы потом её мужественно преодолевать.

    Ну на зачем нужны несколько запросов, когда всё прекрасно собирается в один и не требует транзакции?

    INSERT INTO tickets (user_id, price ...)
    SELECT users.user_id, payment.cost, ...
    FROM users 
    CROSS JOIN (SELECT ?) AS payment (cost)
    WHERE users.user_id = ?
      AND users.credit_limit >= payment.cost
      AND ...

    Тут не надо ни организовывать транзакцию, ни думать об уровне изоляции... А если кредит превысить, вставится ноль записей, и это без проблем отлавливается из статуса выполнения запроса.

    Сервер БД вообще-то оптимизирован на одновременную работу нескольких независимых соединений с одними и теми же данными. Просто не мешайте ему.


    1. dandykry
      02.10.2024 04:59

      что будет, если соседнее соединение одновременно с этим изменит credit_limit?или 100 соединений будут проверять остаток кредитного лимита и успешно его пройдут? точно ничего блокировать не нужно?


      1. Akina
        02.10.2024 04:59

        что будет, если

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


    1. vic_1
      02.10.2024 04:59

      Insert вне транзакции не будет работать, так что транзакция будет так или иначе


      1. Akina
        02.10.2024 04:59

        Да ну? а мужики-то не знают...

        Куча народу выполняет INSERT-запросы, в принципе даже не организуя/начиная транзакцию - и почему-то у них у всех работает. Что мы все делаем не так?


        1. Areso
          02.10.2024 04:59

          Давайте зададим некие предусловия:
          мы говорим о современных реляционных базах, скажем, Оракле, Постгресе и Мускуле (InnoDB).
          То, что вы делаете db_connect.execute("INSERT INTO () VALUES ()") без ручного открытия транзакции, не означает, что ее нет - просто происходит автокоммит. Но он происходит. Коммит же фиксирует (закрывает) транзакцию.
          Конечно, если углубляться в дебри, можно устроить настоящие дебаты, но если использовать определенные упрощения, то это именно так.


          1. Akina
            02.10.2024 04:59

            То, что вы делаете db_connect.execute("INSERT INTO () VALUES ()") без ручного открытия транзакции, не означает, что ее нет - просто происходит автокоммит. Но он происходит. Коммит же фиксирует (закрывает) транзакцию.

            Это зависит от того, как именно работает ваш db_connect.execute. Да, то, что методы в дополнение к указанному запросу навернут ещё хренову гору дополнительных запросов, не новость. Но либо метод безусловно формирует явную транзакцию (кстати, при этом он должен либо явно НЕ поддерживать мультизапросы, либо опять же явно указывать в документации, что в мультизапросе запрещены транзакционные запросы), либо это настраивается параметрами метода или соединения. И третий вариант - метод никаких транзакций не формирует, при этом в составе мультизапроса либо в составе последовательных запросов в рамках одного соединения допускаются транзакционные запросы (начать, зафиксировать, откатить, включить/выключить автокоммит и т.п.).

            Так что заявлять, что транзакция формируется всегда - немножко неправильно. Зависит от реализации и/или настроек.

            мы говорим о современных реляционных базах, скажем, Оракле, Постгресе и Мускуле (InnoDB).

            Например, в случае MySQL вы можете включить General log и в нём посмотреть абсолютно все запросы, которые были посланы клиентским кодом. Там сразу и будет видно, формируется ли описываемая вами транзакция или нет. Скорее всего, в других СУБД тоже имеется такая возможность. Или, скажем, такую возможность может предоставлять драйвер доступа к данным.


            1. Areso
              02.10.2024 04:59

              Ну если вы настаиваете, то вы можете вообще зайти клиентом мускуля в БД, и сделать INSERT INTO.
              В логе у вас будет только INSERT INTO (ну и факт входа в аудите, если настроен), а транзакция при этом все равно будет, просто "под капотом". Только в этот раз - не драйвера, а самого движка СУБД.
              Вы не можете (с InnoDB) сделать вставку без механизма транзакции, это краегоугольный камень ACID реализации.


              1. Akina
                02.10.2024 04:59

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