В цій статті ми поговоримо про транзакції в MongoDB. Як вони працюють? Коли вони необхідні? Як їх налаштувати? Також я поділюсь корисними порадами, щоб уникнути проблем у вашому коді при використанні транзакцій MongoDB.
Фото від Growtika на Unsplash
Що таке транзакція?
Існує багато визначень транзакцій баз даних, але для спрощення можна сказати, що:
Транзакція бази даних — це групування змін в БД, яке розглядається як одна атомарна операція — або всі зміни застосовуються, або всі вони скасовуються за один раз.
Транзакції в SQL та NoSQL базах даних
Транзакція БД — це поняття, яке переважно асоціюється з реляційними базами даних, з усіма характеристиками ACID і чудовою підтримкою, наданою найпопулярнішими SQL базами даних.
Що стосується рішень NoSQL, коли ми маємо багато вузлів, реплікованих наборів і шард — підтримка транзакцій може бути складною — не завжди є підтримка або є деякі обмеження та підводні камені.
Чи підтримує MongoDB транзакції?
Так, вже деякий час кожна основна версія MongoDB підтримує концепцію розподілених транзакцій.
"Розподілені" означає транзакцію, що охоплює кілька документів з однієї або різних колекцій.
Лише в таких випадках нам насправді потрібно створювати транзакцію.
Це тому, що операції з одним документом атомарні за замовчуванням у MongoDB. Це важлива інформація, якою ми повинні скористатися. Давайте розглянемо це нижче.
№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 способи створення транзакції:
- Декларативний спосіб — анотація @Transactional
- Програмний спосіб — використання TransactionTemplate.
Особисто я надаю перевагу та рекомендую спробувати програмний спосіб — тобто просто використовуючи метод transactionTemplate.execute { } та маючи повний контроль над межами транзакції, оскільки з мого досвіду це призводить до менших проблем і є більш зрозумілим, а також уникає "магії" анотацій у Spring Boot.
Переваги програмного способу:
- Більш виразні та явні межі транзакції.
- Легший контроль за тим, який код потрапляє в межі транзакції.
- Легше зрозуміти, коли транзакція створена, а коли ні.
Крім того, існують деякі потенційні проблеми з декларативним способом:
- За допомогою @Transactional легко можна помилково використовувати анотацію на методі верхнього рівня сервісу, змішуючи транзакцію з операціями вводу/виводу.
- Метод з анотацією @Transactional має бути публічним.
- Створюється проксі-об’єкт Spring.
- Внутрішні виклики методів з @Transactional ігноруються.
- Використання @Transactional для самовиклику не створює транзакцію, якщо не використовувати режим AspecJ.
Давайте подивимося, як це може виглядати в коді, коли ми використовуємо програмний спосіб керування транзакціями в базі даних.
По-перше, ми повинні додати рядок з'єднання MongoDB до вашого файлу application.yml.
По-друге, ми повинні увімкнути підтримку транзакцій, оскільки за замовчуванням вона вимкнена в MongoDB, для цього ми можемо додати @Bean для MongoTransactionManager:
Налаштування та конфігурація менеджера транзакцій 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 ми можемо поставити собі такі запитання:
- Чи хочу я зчитувати дані, додані іншою транзакцією, але ще не зафіксовані (брудне зчитування)? Використовуйте READUNCOMMITTED_ — зазвичай це небажано, оскільки це також дозволяє фантомні зчитування та незворотні зчитування.
- Чи хочу я зчитувати лише зафіксовані дані? Використовуйте READCOMMITTED_
- Чи хочу я уникнути ситуації, коли дані, які я щойно зчитав, змінюються іншою транзакцією — друге зчитування відрізняється від першого? Використовуйте REPEATABLEREAD_ щоб уникнути ситуації незворотного зчитування.
- Крім REPEATABLEREAD, чи хочу я уникнути ситуації другого зчитування з новими даними, доданими іншою транзакцією тим часом? Використовуйте _SERIALIZABLE
MongoDB має іншу концепцію ізоляції.
Оскільки ми можемо мати шардовані кластери та реплікаційні набори з багатьма вузлами (основними та вторинними вузлами), в такому розподіленому середовищі існують деякі незрозумілості щодо того, що гарантується, а що є відкладено узгодженим.
текст перекладу
Часто ми використовуємо концепцію “більшості” вузлів, які підтвердили зміну.
Тому, щоб забезпечити ізоляцію в MongoDB, ми повинні використовувати налаштування read і write concern.
Зазвичай ми хочемо уникнути всіх вищеописаних проблем під час зчитування даних і бажаємо мати найбільш суворий рівень ізоляції — він називається “snapshot” і є подібним до рівня ізоляції “serialized” в SQL.
Для досягнення цього в MongoDB та щоб уникнути фантомних зчитувань, незворотних зчитувань і брудних зчитувань ми використовуємо комбінацію наступних налаштувань read і write concern:
або за допомогою декларативного способу:
@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