JavaRush /Курси /Модуль 5. Spring /Лекція 211: Проблеми управління транзакціями в розподілен...

Лекція 211: Проблеми управління транзакціями в розподілених системах

Модуль 5. Spring
Рівень 14 , Лекція 0
Відкрита

Переваги Event-Driven Architecture

Ми звикли до того, що традиційні додатки працюють як діалог: запит-відповідь, запит-відповідь. Але що якщо нам потрібна система, яка працюватиме як сучасний месенджер? Де повідомлення приходять, коли щось сталося, а не коли ми постійно питаємо «є щось новеньке?»

Event-Driven Architecture (EDA) — це як перейти від телефонних дзвінків до групового чату. У старій системі вам треба було дзвонити кожному учаснику окремо, чекати відповіді, а якщо хтось не бере трубку — весь процес застрягає. У чаті ж ви просто надсилаєте повідомлення, і всі зацікавлені учасники його отримують.

Такий підхід дає дивовижну гнучкість. Додати нового учасника в чат? Пара кліків, і він уже в курсі подій (scalaibility). Хтось тимчасово не онлайн? Нічого страшного, підключиться пізніше і прочитає пропущені повідомлення (fault tolerance). Кожен учасник сам вирішує, на які повідомлення реагувати, а які пропустити (loose coupling). І все це відбувається миттєво, без зайвих затримок і узгоджень (reactivity).

Звісно, налаштувати таку систему складніше, ніж простий телефонний дзвінок. Але коли вона запрацює, ви отримаєте гнучке і масштабоване рішення, готове до будь-яких змін.

Основні характеристики розподілених систем:

  • Багато вузлів: дані та логіка розкидані по різних сервісах.
  • Незалежність компонентів: кожен сервіс може виконувати свою задачу без прямої залежності від інших.
  • Асинхронні взаємодії: сервіси можуть "спілкуватися" між собою через події або повідомлення.

Все виглядає чудово... поки не з'являються проблеми з координацією, особливо коли доходить до транзакцій.


Чому виникають труднощі з транзакціями?

У монолітних додатках транзакції досить прості. Наприклад, Spring і Hibernate дозволяють просто повісити анотацію @Transactional, і магія відбувається за лаштунками. Ви оновлюєте дані в одній таблиці, потім в іншій, і Spring подбає про те, щоб усе відбувалося атомарно: або всі зміни застосовуються, або нічого не відбувається.

Але в розподілених системах це вже не так просто. Причина? Сервіси працюють в ізоляції один від одного, і кожен з них може мати власну базу даних.


Проблеми в розподілених транзакціях

  1. Консистентність даних: як переконатися, що дані в різних сервісах "синхронізовані", якщо основний сервіс впав, а зміни в іншому сервісі вже застосовані?
  2. Повторюваність операцій: якщо транзакція частково завершилася, як "відкотити" вже зроблені зміни?
  3. Мережеві затримки та проблеми: комунікації між сервісами залежать від мережі, тут виникають тайм-аути, втрачені повідомлення або дублюючі запити.
  4. Незалежність сервісів: нам не хочеться, щоб збій в одному сервісі "валив" всю систему.

Уявіть ситуацію: у вас є мікросервіс для обробки замовлень, інший — для списання грошей з банківського рахунку, і третій — для відправки повідомлень. Що буде, якщо третій сервіс вийде з ладу після списання грошей, але до відправки повідомлення? Платіж пройшов, а клієнт думає, що нічого не сталося. Це фіаско.


Проблеми координації між сервісами

А що, якщо щось піде не так? У монолітному додатку все просто: Spring сам розрулює транзакції. Поставив анотацію @Transactional — і спи спокійно. Але в розподіленому світі все набагато складніше.

Уявіть, що ви замовляєте піцу. У моноліті це як дзвінок в одну піцерію: вони самі перевірять наявність інгредієнтів, оформлять замовлення, приймуть оплату і організують доставку. Якщо на будь-якому етапі щось піде не так — замовлення просто скасують.

У розподіленій системі кожен крок може виконувати окремий сервіс: один перевіряє склад, інший обробляє оплату, третій займається доставкою. І тут починається найцікавіше:

  1. Сервіс оплати каже «деньги списаны», а сервіс доставки раптово не відповідає. Що робити з грошима?
  2. Кур'єр вже в дорозі, а виявляється, що оплата не пройшла. Викликати кур'єра назад?
  3. Через збій мережі замовлення випадково оформилося двічі. Як не привезти клієнту дві піци?

Чому це складно?

На відміну від моноліту, де всі процеси контролюються централізовано, в розподіленій системі кожен сервіс працює незалежно. Немає єдиного центру, який міг би сказати: «Гаразд, все пішло не за планом, давайте відкотимо назад».

Кожен сервіс знає тільки про свою частину роботи. Сервіс оплати не в курсі, що відбувається з доставкою, а сервіс доставки не знає, чи пройшла оплата. І коли щось іде не так, координувати їх дії стає справжнім викликом.

Приклади реальних проблем:

  1. Втрата повідомлень: один сервіс відправляє запит іншому, але повідомлення губиться в мережі. Як переконатися, що зміни були застосовані?
  2. Неповний відкат: якщо один сервіс успішно зробив свою частину роботи, а інший зламався, як повернути все до початкового стану?
  3. Дублювання операцій: уявімо, сигнал про списання грошей був відправлений двічі через збій мережі. Як уникнути подвійного списання?

Стратегії вирішення проблем

Не переймайтесь! Всі ці проблеми можна вирішити, якщо підходити інженерно. Ось кілька стратегій, які розробники використовують для управління транзакціями в розподілених системах:

Двофазний коміт (2PC)

Цей метод координує різні сервіси так, щоб вони «голосували» за успіх транзакції. Фаза 1: всі сервіси кажуть «так, я готовий». Фаза 2: координатор підтверджує зміни (або відкочує їх, якщо хтось сказав «ні»). Здається просто, але на практиці складно, бо:

  • 2PC не підходить для високонавантажених систем через низьку продуктивність.
  • Якщо координатор виходить з ладу, усе може піти наперекосяк.

Приклад тривіального (і доволі повільного) підходу.

@Transactional
public void performDistributedTransaction(List<Service> services) {
    try {
        for (Service service : services) {
            service.prepare(); // Фаза 1: підготовка
        }
        for (Service service : services) {
            service.commit(); // Фаза 2: підтвердження змін
        }
    } catch (Exception e) {
        services.forEach(Service::rollback); // У разі помилки: відкат
    }
}

Патерн саги

Патерн саги вирішує проблему транзакцій у дусі: «Давайте розбиратися крок за кроком». Ми розділяємо процес на набір дій, а якщо щось іде не так, запускаємо компенсаційні дії. Наприклад:

  • Сервіс 1: «Я списав гроші».
  • Сервіс 2: «Я додав товар у корзину».
  • Якщо обидва сервіси кажуть «готово» — успіх. Якщо другий сервіс ламається, перший має «відкотити» свою дію — наприклад, повернути гроші на рахунок.

Ось як може виглядати базова "сага" (більше деталей буде на наступній лекції!):

public void processSaga() {
    try {
        firstService.doAction();
        secondService.doAction();
    } catch (Exception e) {
        firstService.compensate(); // Відкат зміни у разі помилки
    }
}

Патерн саги стає особливо потужним, якщо використовувати його з подійно-орієнтованою архітектурою (наприклад, з Kafka).


Event Sourcing

Цей підхід каже: «Запам'ятовуйте лише події, а не їх результат». Наприклад:

  • Ви не зберігаєте суму на рахунку, а зберігаєте кожну окрему подію: "Поповнення", "Списання", "Повернення".
  • Якщо щось пішло не так, ви просто можете "перепроіграти" події і відновити стан системи.

Event Sourcing відмінно поєднується з CQRS (про це поговоримо в одній з наступних лекцій!) і Kafka.


Наглядачі і таймаути

Простий, але ефективний підхід. Якщо ви надсилаєте запит в інший сервіс і не отримуєте відповіді за певний час, ви відкочуєте зміни. Головне тут — грамотна реалізація таймаутів і спроб повторної відправки.


Підсумки

На практиці вибір підходу залежить від ваших вимог:

  • Якщо потрібна сувора консистентність, можливо, 2PC — ваш вибір (хоча варто уникати цього в високонавантажених системах).
  • Якщо ви готові пожертвувати консистентністю заради продуктивності, беріть на озброєння патерн саги.
  • Якщо хочете максимальної гнучкості і відмовостійкості, зверніть увагу на Event Sourcing.

Ключова ідея, яку ви повинні зрозуміти: у розподілених системах не буває ідеальних рішень. Замість цього ви обираєте компроміс між продуктивністю, відмовостійкістю та рівнем консистентності даних.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ