Apache Seata превращает распределенные транзакционные кошмары в Spring Boot Dreams

Apache Seata превращает распределенные транзакционные кошмары в Spring Boot Dreams

11 июня 2025 г.

В то время, когда ностальгия, возможно, зашла слишком далеко, мир разработки программного обеспечения был охвачен горячими дебатами: имеет массовое принятиеМикросервисыОбразец действительно обеспечил ожидаемые выгоды, или его баланс более неопределен? Многие команды начинают задаваться вопросом, пришло ли время «возвращения на родину» до старого старого, надежного монолита - или если ответ находится где -то посередине, в форме модульного монолита.
За исключением фактора стоимости, это переосмысление часто связано с неотъемлемой сложностью распределенных систем, особенно одной из их важнейших болевых точек:Распределенные транзакцииПолем

Эта статья направлена ​​на то, чтобы точно решить эту тему, демонстрируя, какApache SeataВ этом случае в сочетании с ловкостью весеннего ботинка удается преобразовать то, что для многих является кошмаром в удивительно управляемое решение. Я покажу вам, как Seata может помочь вам крепко спать по ночам, устраняя необходимость в сложных архитектурах и стратегиях отката и/или прибегнуть к более громоздким узорам, таким какШаблон исходящих ящиковПолем

A nostalgic and skeptical architect

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

Основными компонентами являются:

Координатор транзакций (TC): поддерживать статус глобальных и филиалов, стимулируйте глобальный коммит или откат. Менеджер транзакций (TM): Определите объем глобальной транзакции: начинайте глобальную транзакцию, совершать или откатить глобальную транзакцию. Менеджер ресурсов (RM). Поддерживаемые модели транзакций: AT, XA (2pc) и Saga.

(Если вы предпочитаете пропустить теорию, перейдите прямо к практическому решению!)

XA (2pc) XA является спецификацией, выпущенной в 1991 году X/Open (которая впоследствии объединилась с открытой группой).

Фаза 1: подготовить (Commit voal): TC (Seata Server), спрашивает все участвующие RMS (например, ваши микросервисы, взаимодействующие с базами данных), если они готовы совершить свою локальную транзакцию. Каждый RM выполняет свои операции, записывает журналы транзакций для обеспечения долговечности и блокирует ресурсы для гарантирования изоляции. Если RM готов, он отвечает «да» на TC. Если нет, это отвечает «нет». 2. Фаза 2: Коммит или откат:

Если все RMS отвечают «да», TC отправляет команду «Commit» на все RMS. Они завершают свои локальные транзакции и выпускают замки. Если какой -либо RM отвечает «нет», или если TC обнаруживает тайм -аут/сбой, TC отправляет команду «откат». Все среднеквадратичные помещения отменили их операции и выпускают замки. Плюсы и минусы (кратко):

Pro: обеспечивает прочную консистенцию данных (кислоту) для нескольких услуг, действуя как одна нерушимая транзакция. CON: может привести к блокированию ресурсов (высокая задержка), если участники или координатор являются медленными или сбой, потенциально влияя на доступность и масштабируемость. Он также опирается на базовые базы данных/ресурсы, поддерживающие стандарт XA. Сага Паттерн Саги является широко принятым подходом в архитектурах микросервисов для управления распределенными транзакциями. В отличие от 2pc, Saga жертвует немедленной сильной последовательности для более высокой доступности и масштабируемости, достигая возможной последовательности.

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

Никаких глобальных замков. Крайне, локальные транзакции сразу совершают и не имеют глобальных замков, что позволяет повысить параллелизм. Компенсация за неудачу: если какая -либо локальная транзакция не удается, сага не «откатана» в традиционном смысле. Вместо этого он выполняет серию компенсационных транзакций, чтобы семантически отменить эффекты ранее завершенных локальных транзакций. Эти компенсационные транзакции являются новыми операциями, предназначенными для обращения за влиянием на бизнес. Сага может быть реализована через:

Хореография: Услуги публикуют события и реагируют на них, что приводит к децентрализованному потоку. Оркестрация: центральный оркестратор координирует поток, отправляя команды и реагируя на ответы. Плюсы и минусы (кратко):

Pro: Отлично подходит для высокой доступности и масштабируемости из-за отсутствия давних распределенных замков. Идеально подходит для слабо связанных микросервисов. CON: достигает возможной последовательности, то есть данные могут быть временно непоследовательными. Требует значительных усилий по разработке для реализации всех компенсационных транзакций и управления сложной логикой саги, которая также может усложнить отладку. В режиме AT (автоматическая транзакция) есть флагманское решение Seata, направленное на то, чтобы предложить легкость использования 2 % с неблокирующим характером и масштабируемостью пособий SAGA. Это рекомендуемый дефолт для большинства микросервисов с использованием реляционных баз данных.

Фаза 1: Локальная транзакция и подготовка: когда микросервис (RM) выполняет операцию базы данных (например, обновление, вставка, удаление) в рамках глобальной транзакции: интеллектуальная данных Seata, перехватывает SQL. Он автоматически создает undo_log (запись состояния данных до модификации). Операция SQL выполняется сразу же в локальной базе данных. Затем Seata приобретает глобальный замок для модифицированного ресурса через координатора транзакций (TC). Эта блокировка не является традиционной блокировкой базы данных; Это предотвращает одновременно изменять другие глобальные транзакции, но не блокирует операции чтения. RM сообщает TC, что ее транзакция в отрасли «подготовлена». 2. Фаза 2: Глобальный коммит или откат:

Глобальный коммит: если все транзакции филиалов успешно готовятся, ТС инструктирует их совершать. Поскольку местные транзакции DB уже были совершены на фазе 1, RMS просто выпускает свои глобальные замки. Глобальный откат: если какая -либо филиальная транзакция не удается, или глобальная транзакция должна отказываться: TC инструктирует RMS обратно. RMS использует их сохраненный undo_log, чтобы автоматически компенсировать изменения, внесенные в их локальные базы данных, эффективно восстанавливая предыдущее состояние. Затем они выпускают свои глобальные замки. Плюсы и минусы (кратко):

Pro: обеспечивает сильную последовательность для глобальной транзакции. Предлагает отличную доступность и масштабируемость, поскольку локальные блокировки базы данных удерживаются лишь кратко. Это очень прозрачно для разработчиков, требующих минимальных изменений кода. Автоматический откат упрощает обработку ошибок. Con: в основном разработан для реляционных баз данных. В то время как не блокируя на уровне БД, все еще существует накладные расходы от создания undo_logs и управления глобальными замками.

An impatient developer

Практическая демонстрация с весенним загрузкой

Давайте представляем сценарий с участиемДва различных микросервиса, каждый работает со своей собственной выделенной и автономной базой данных:

  • Кредитный API: Отвечает за управление кредитом пользователя (баланс денег)
  • Доставка API: Посвящается обработке покупок отгрузки

Орчест на эти две услугиBFF (бэкэнд для Frontend)Полем Его роль состоит в том, чтобы координировать операцию по покупке отгрузки, что переводится на последовательность распределенных вызовов:

  • Проверка и/или вычитать пользовательский кредитчерез кредитный API.
  • На самом деле покупка отправкиИспользуя кредит, через транспортный API.

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

A neighbor

Архитектура


ТМ

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

@Service
public class BFFManager {

    @Autowired
    private CreditService creditService;

    @Autowired
    private ShippingService shippingService;

    @GlobalTransactional
    public ShippingResult buyShipping(Long userID, BigDecimal cost) {
        var wallet = creditService.updateBalance(userID, cost);
        var shipping = shippingService.buyShipping(userID, cost);
        
        wallet = creditService.getWallet(userID);

        var result = new ShippingResult();
        result.setCost(cost);
        result.setShippingID(shipping.getId());
        result.setCurrentBalance(wallet.getBalance());
        return result;
    }
}

Так что@GlobaltransactionalАннотация достаточно? Конечно, нет, но мало что нужно:

Зависимости, необходимые для TM:

implementation 'org.apache.seata:seata-spring-boot-starter:2.3.0'
implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-seata:2023.0.3.3') {
  exclude group: 'org.apache.seata', module: 'seata-spring-boot-starter'
}

Весенний-клуб-Starter-Alibaba-Seata, среди прочего, он может сделать, гарантирует, что HTTP -связь между микросервисами всегда содержатГлобальный идентификатор транзакции (Xid)

Seata-Spring-Boot-Starterэто классический стартовый стартер весеннего ботинка, который автоконфигурирует сущность Seata (в данном случае TM), начиная с свойств:

seata:
  enabled: true
  data-source-proxy-mode: AT
  enable-auto-data-source-proxy: true
  application-id:  ${spring.application.name}
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

Rm

Кредитный APIисудоходство-апидействовать как rm. Им нужна только зависимостьSeata-Spring-Boot-StarterС следующими свойствами:

seata:
  enabled: true
  data-source-proxy-mode: AT
  enable-auto-data-source-proxy: true
  application-id:  ${spring.application.name}
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091

Необходимо, чтобы RM DB содержалundo_logСтол для Seata.Здесьявляются необходимыми сценариями для каждого типа дБ.

В коде, который вы найдете на GitHub, создание таблицы управляется через композицию Docker, которая создает выделенный DB (через org.springframework.boot: спружите

В RMS не требуется конкретная аннотация. Напишите свой код в хранилище, как всегда. Если вы хотите, и я рекомендую это, продолжайте использовать@TransactionalДля местных транзакций.

ТК

ТС представлен Сердцем сито,Seata-сервер.Это требует двух основных конфигураций:

  • Реестр: определяетРеестр услугЭто будет использовать Seata (Nacos, Eureka, Consul, Zookeper, Redis, File). Для этого примера я решил использоватьфайлтип
  • Хранить: определяет постоянство данных о глобальных транзакциях и глобальных замков (файл, DB, Redis). Для этого примера я решил использоватьдБтип.

Вот настройка Docker Compose, используемая для инициализации сервера:

services:
  mysql:
    image: mysql:8.0.33
    container_name: mysql-seata
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: seata
      MYSQL_USER: seata_user
      MYSQL_PASSWORD: seata_pass
    ports:
      - "3317:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/seata/mysql.sql:/docker-entrypoint-initdb.d/seata.sql:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-prootpass"]
      interval: 10s
      timeout: 5s
      retries: 5

  seata-server:
    image: apache/seata-server:2.3.0
    container_name: seata-server
    depends_on:
      mysql:
        condition: service_healthy
    environment:
      - SEATA_CONFIG_NAME=application.yml
    volumes:
      - "./docker/seata/resources/application.yml:/seata-server/resources/application.yml"
      - "./docker/seata/mysql-connector-j-8.0.33.jar:/seata-server/libs/mysql-connector-j-8.0.33.jar"
    ports:
      - "7091:7091"
      - "8091:8091"

volumes:
  mysql_data:

И свойства конфигурации (Application.yaml):

server:
  port: 7091

spring:
  application:
    name: seata-server
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://mysql:3306/seata
    username: seata_user
    password: seata_pass

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${log.home:${user.home}/logs/seata}

console:
  user:
    username: seata
    password: seata

seata:
  security:
    secretKey: seata
    tokenValidityInMilliseconds: 1800000

  config:
    type: file

  registry:
    type: file

  store:
    mode: db
    db:
      dbType: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://mysql:3306/seata
      user: seata_user
      password: seata_pass
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      vgroup-table: vgroup_table
      query-limit: 1000
      max-wait: 5000

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

Играть!

@SpringBootTest
public class GlobalTransactionalTest {

    @Autowired
    private BFFManager bffManager;

    @Autowired
    private CreditService creditService;

    @MockitoBean
    private ShippingService shippingService;

    @Test
    public void globalTransactionalTest_OK() {
        var wallet = creditService.getWallet(1L);
        var shipping = new Shipping();
        shipping.setId(2L);
        when(shippingService.buyShipping(1L, new BigDecimal(4))).thenReturn(shipping);
        bffManager.buyShipping(1L, new BigDecimal(4));
        var newWallet = creditService.getWallet(1L);
        assertEquals(new BigDecimal("4.00"), wallet.getBalance().subtract(newWallet.getBalance()));
    }

    @Test
    public void globalTransactionalTest_KO() {
        var wallet = creditService.getWallet(1L);
        var shipping = new Shipping();
        shipping.setId(2L);
        when(shippingService.buyShipping(1L, new BigDecimal(4))).thenThrow(new RuntimeException());

        try {
            bffManager.buyShipping(1L, new BigDecimal(4));
        } catch (Exception e) {}

        var newWallet = creditService.getWallet(1L);
        assertEquals(newWallet.getBalance(), wallet.getBalance());
    }

}

Полный иработающийКод доступен наGitHubПолем Запустите компоненты и дайте мне знать, что вы думаете!

Ключевые альтернативы

  • Атомикос: Xa (2pc). Он работает только в одном и том же микросервисе, который обращается к нескольким базам данных (нет координатора транзакций, нет распространения XID)
  • Двигатель рабочего процесса для саги -оркестровки (КамндаВThemoral.io): Требуется внешний оркестратор и более высокие усилия по интеграции.
  • SAGA (хореографическое событие): возможный компромисс.
  • Образец Outbox: возможная последовательность.

Эта статья не направлена ​​на то, чтобы продвигать Apache Seata по сравнению с другими упомянутыми альтернативами, а скорее подчеркнуть еепростота использованияПолем Как всегда, правильный инструмент должен быть выбран на основе конкретного контекста и системных требований.

Следующий эпизод: «Насколько круты были монолиты? »Следите за обновлениями.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE