Транзакції MongoDB у Spring Boot

В цій статті ми поговоримо про транзакції в MongoDB. Як вони працюють? Коли вони необхідні? Як їх налаштувати? Також я поділюсь корисними порадами, щоб уникнути проблем у вашому коді при використанні транзакцій MongoDB.

pic

Фото від Growtika на Unsplash

Що таке транзакція?

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

Транзакція бази даних — це групування змін в БД, яке розглядається як одна атомарна операція — або всі зміни застосовуються, або всі вони скасовуються за один раз.

Транзакції в SQL та NoSQL базах даних

Транзакція БД — це поняття, яке переважно асоціюється з реляційними базами даних, з усіма характеристиками ACID і чудовою підтримкою, наданою найпопулярнішими SQL базами даних.

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

Чи підтримує MongoDB транзакції?

Так, вже деякий час кожна основна версія MongoDB підтримує концепцію розподілених транзакцій.
"Розподілені" означає транзакцію, що охоплює кілька документів з однієї або різних колекцій.
Лише в таких випадках нам насправді потрібно створювати транзакцію.

Це тому, що операції з одним документом атомарні за замовчуванням у MongoDB. Це важлива інформація, якою ми повинні скористатися. Давайте розглянемо це нижче.

pic

№1. Документація Spring Boot Data MongoDB

Як спроектувати сутність MongoDB в Spring Boot?

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

Добре спроектована система — це ключ до успіху, тому при створенні документів MongoDB не потрібно вагатися і варто дотримуватись патерну агрегаційного кореня (DDD aggregate root) і вбудовувати дочірні об'єкти в наш основний об'єкт домену.
Те, що є нормалізованим графом сутностей SQL з кількома таблицями, в NO SQL зазвичай повинно бути сховано в одному документі.
Це продуктивніше і простіше — просто зберігати всі зв'язані об'єкти разом і захоплювати ці зв'язки в одному документі.
MongoDB стверджує, що вона спроектована для обробки таких великих документів без проблем.
Отже, для MongoDB — вбудовування об'єктів і збереження зв'язків між ними в одному документі є дієвим і простим способом забезпечення цілісності даних.

Приклад:

@Document(ORDER_UPDATES_DOCUMENT)  
data class DeliveryPlanningDocument(  
 @Id  
 val id: String? = null,  
 val routes: List  
 val finalized: Boolean = false,  
... будь-які інші поля  
 @CreatedDate  
 var createdAt: LocalDateTime? = null,  
 @LastModifiedDate  
 var dateChanged: LocalDateTime? = null,  
 @Version  
 val version: Long? = null,  
)  

data class Route(  
 val id: String,  
 val routePlan: RoutePlanning,  
 val vehicleDetails: VehicleDetails  
 val areaInformation: AreaDetails  
... будь-яке інше поле  
)

В такій сутності, якщо ми змінимо DeliveryPlanningDocument і збережемо його — всі Routes будуть збережені разом з ним як одна атомарна операція — або все буде оновлено, або нічого — наш документ залишається консистентним.

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

Наприклад, разом з додаванням нового DeliveryPlanning, нам слід оновити інформацію про транспортний засіб або статус процесу генерації планування.
текст перекладу
Or we just have multiple route plannings we must update or save together — in those cases we should create and manage MongoDB “distributed transaction”.

Як створити транзакцію в базі даних MongoDB та Spring Boot?

У Spring Boot є 2 способи створення транзакції:

  1. Декларативний спосіб — анотація @Transactional
  2. Програмний спосіб — використання TransactionTemplate.

Особисто я надаю перевагу та рекомендую спробувати програмний спосіб — тобто просто використовуючи метод transactionTemplate.execute { } та маючи повний контроль над межами транзакції, оскільки з мого досвіду це призводить до менших проблем і є більш зрозумілим, а також уникає "магії" анотацій у Spring Boot.

Переваги програмного способу:

  1. Більш виразні та явні межі транзакції.
  2. Легший контроль за тим, який код потрапляє в межі транзакції.
  3. Легше зрозуміти, коли транзакція створена, а коли ні.

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

  1. За допомогою @Transactional легко можна помилково використовувати анотацію на методі верхнього рівня сервісу, змішуючи транзакцію з операціями вводу/виводу.
  2. Метод з анотацією @Transactional має бути публічним.
  3. Створюється проксі-об’єкт Spring.
  4. Внутрішні виклики методів з @Transactional ігноруються.
  5. Використання @Transactional для самовиклику не створює транзакцію, якщо не використовувати режим AspecJ.

Давайте подивимося, як це може виглядати в коді, коли ми використовуємо програмний спосіб керування транзакціями в базі даних.

По-перше, ми повинні додати рядок з'єднання MongoDB до вашого файлу application.yml.

По-друге, ми повинні увімкнути підтримку транзакцій, оскільки за замовчуванням вона вимкнена в MongoDB, для цього ми можемо додати @Bean для MongoTransactionManager:

pic

Налаштування та конфігурація менеджера транзакцій MongoDB в класі Spring Boot @Configuration

Тепер ми нарешті можемо використовувати це для створення нашої першої транзакції:

@Service   
class OurService(   
private val mongoTransactionTemplate: TransactionTemplate,  
private val repository: OrderRepository,  
private val repository2: ClientRegistrationRepository) {  
 fun doSomething() {  
 mongoTransactionTemplate.execute {  
 repository.save(newOrder)  
 repository2.save(newClient)   
 }   
}

Як налаштувати транзакцію в MongoDB та Spring Boot?

Конфігурація транзакції MongoDB за допомогою @Transactional

Якщо ви використовуєте декларативний спосіб створення транзакції Spring Boot за допомогою анотації @Transactional, ви можете налаштувати все, як у SQL транзакції:

  • параметр readOnly
  • рівень ізоляції
  • пропагування
  • тайм-аути
  • поведінка при скасуванні

Специфічні налаштування MongoDB можна передати через анотацію @Transactional за допомогою параметра label, наприклад:

@Transactional(label = { "mongo:readConcern=available" })

Більше інформації тут:
https://docs.spring.io/spring-data/mongodb/reference/mongodb/client-session-transactions.html#mongo.transaction.options

Декларативний спосіб конфігурації транзакції MongoDB

Як ви бачите на наведеній вище картинці №2, при використанні менеджера транзакцій Mongo можна передавати подібні/ті самі налаштування або в transactionTemplate, або безпосередньо в сам менеджер транзакцій через об'єкт MongoTransactionOptions.

Для стандартних налаштувань Spring Boot вони виглядають так:

  • write concern: { w: 1 } — це означає підтвердження операції запису на 1 вузлі без очікування на реплікацію на всі вузли.
    Інший варіант — "Majority", що є найбільш суворим налаштуванням, даючи найбільш ізольовану транзакцію та гарантуючи правильний запис даних.
  • read concern: local — немає гарантії, що отримані дані записані на більшість вузлів.
    текст перекладу
    “Snapshot” налаштування є найбільш суворим — дає найкращу ізоляцію вашої транзакції — зчитування на основі більшості вузлів у стані даних.
  • read preference: primary — для зчитування буде використовуватися основний вузол — документація MongoDB стверджує: Розподілені транзакції які містять операції зчитування, повинні використовувати read preference primary. Всі операції в одній транзакції повинні бути спрямовані до одного й того ж члена.”

Якщо ваша програма має високе навантаження або потребує високої узгодженості — важливо налаштувати параметри MongoDB.

Наприклад, коли ви хочете мати більшу надійність і узгодженість — розгляньте налаштування write concern на “majority” — тоді ваші дані будуть точно репліковані на багато вузлів MongoDB.

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

У кінцевому підсумку — завжди важливо глибоко розуміти рівень ізоляції вашої транзакції.

Ізоляція транзакцій у MongoDB та SQL

У світі SQL ми можемо поставити собі такі запитання:

  1. Чи хочу я зчитувати дані, додані іншою транзакцією, але ще не зафіксовані (брудне зчитування)? Використовуйте READUNCOMMITTED_ — зазвичай це небажано, оскільки це також дозволяє фантомні зчитування та незворотні зчитування.
  2. Чи хочу я зчитувати лише зафіксовані дані? Використовуйте READCOMMITTED_
  3. Чи хочу я уникнути ситуації, коли дані, які я щойно зчитав, змінюються іншою транзакцією — друге зчитування відрізняється від першого? Використовуйте REPEATABLEREAD_ щоб уникнути ситуації незворотного зчитування.
  4. Крім REPEATABLEREAD, чи хочу я уникнути ситуації другого зчитування з новими даними, доданими іншою транзакцією тим часом? Використовуйте _SERIALIZABLE

MongoDB має іншу концепцію ізоляції.
Оскільки ми можемо мати шардовані кластери та реплікаційні набори з багатьма вузлами (основними та вторинними вузлами), в такому розподіленому середовищі існують деякі незрозумілості щодо того, що гарантується, а що є відкладено узгодженим.
текст перекладу
Часто ми використовуємо концепцію “більшості” вузлів, які підтвердили зміну.

Тому, щоб забезпечити ізоляцію в MongoDB, ми повинні використовувати налаштування read і write concern.

Зазвичай ми хочемо уникнути всіх вищеописаних проблем під час зчитування даних і бажаємо мати найбільш суворий рівень ізоляції — він називається “snapshot” і є подібним до рівня ізоляції “serialized” в SQL.

Для досягнення цього в MongoDB та щоб уникнути фантомних зчитувань, незворотних зчитувань і брудних зчитувань ми використовуємо комбінацію наступних налаштувань read і write concern:

pic

або за допомогою декларативного способу:

@Transactional(label = { "mongo:readConcern=snapshot", "mongo:writeConcern=majority" })

Більше інформації та пояснень, як працюють різні параметри read і write, можна знайти тут:
https://www.mongodb.com/docs/manual/reference/read-concern/#read-concern-levels
https://www.mongodb.com/docs/manual/reference/write-concern/

Підсумки та TLDR

У цій статті ми розглянули конфігурацію та налаштування транзакцій MongoDB в додатку Spring Boot.

Основні висновки та уроки:

  • Необхідно явно увімкнути транзакції в MongoDB, визначивши MongoTransactionManager @Bean.
  • Більшість практичних випадків НЕ потребують транзакцій, оскільки в MongoDB слід моделювати ваш документ для захоплення зв'язків між об'єктами в 1 сутність.
  • Якщо можливо, використовуйте програмний спосіб для створення транзакції з налаштуванням transactionTemplate — ви матимете більше контролю і більш зрозумілий код без анотацій і магії AOP.
  • Специфічні параметри MongoDB для транзакцій і налаштувань зчитування/запису можна налаштувати через MongoTransactionOptions або @Transactional(label = “mongo:setting”).
  • Розгляньте можливість тонкого налаштування MongoDB і транзакцій, експериментуючи з налаштуваннями read і write concern і рівнем ізоляції транзакцій, щоб досягти бажаних результатів.
  • Для найкращої ізоляції транзакцій — використовуйте read concern “snapshot” і write concern “majority” в MongoTransactionManager або всередині @Transactional(label = {
    “mongo:readConcern=snapshot”, “mongo:writeConcern=majority” })

Якщо ви дійшли до кінця цієї статті — дякую за прочитане, сподіваюся, ви дізналися щось нове або корисне!

Перекладено з: MongoDB transactions with Spring Boot

Leave a Reply

Your email address will not be published. Required fields are marked *