Типовий приклад транзакції, що використовується майже у кожному поясненні – це переказ коштів з одного банківського рахунку на інший. У цьому прикладі передбачається, списання грошей з одного облікового запису та зарахування на інший, тобто йдеться про принаймні дві операції оновлення балансу. При цьому основною вимогою до транзакції є атомарність операцій, тобто неможливість виконання лише одного оновлення. Мають бути виконані або обидва оновлення, або, у випадку якоїсь помилки, будь-які зміни мають бути відкочені назад.
У цій публікації я хочу на типовому прикладі транзакції вказати на недоліки транзакцій та розповісти про те, як можна обійтися без них, зробивши систему більш надійною та ефективною майже безкоштовно.
Транзакція – це зупинка часу
Для мене транзакція схожа на зупинку часу оскільки за її допомогою ми ніби намагаємося позбутися дистанції та об’єднати різні об’єкти в один.
Зрозуміло, що ми не можемо зупинити час насправді і що імітація зупинки часу чогось нам коштує.
Щонайменше, необхідно заблокувати доступ (принаймні на запис) до об’єктів даних (змінних), що задіяні у транзакції та підтримувати існування декількох версій даних (стару, та нову, що змінилася у контексті транзакції, і не має бути видима до успішного завершення транзакції).
Це потребує додаткових записів, обслуговування (у багатьох СУБД оновлення виконується як створення нового запису та позначення старого як неактивного, старий запис пізніше має бути видалено, а сховище данних – дефрагментовано/ущільнено, наприклад, саме для цього і існує операція вакуумації у PostgreSQL), більшої кількості постійної пам’яті (принаймні тимчасово) та більшого використання оперативної пам’яті у якій впродовж усієї транзакції мають зберігатися всі зміни.
Крім того, блокування об’єктів данних (змінних, записів у БД і т.д.) викликає збільшення черги на запис, що також потребує більшої кількості оперативної пам’яті, підвищує затримку обробки запиту/відповіді та може погіршувати досвід користування.
Додатково у нас можуть бути досить складні проблеми пов’язані з часом, наприклад, коли транзакція починається в один звітний період, а закінчується в інший, вже після того, коли звіт було сформовано.
Як бачимо, забезпечення атомарності операцій – це досить складна та затратна справа.
Як позбутися транзакцій?
З теорії вирішення винахідницьких задач я дізнався про цікаву концепцію ідеального об’єкту. Ідеальний об’єкт – це об’єкт якого не існує, але функція якого виконується. Отже ідеальним рішенням має бути таке, що виключає використання транзакцій, при цьому гарантуючи атомарність. Як це можливо?
Ключ до відмови від транзакцій знаходиться у тому, для чого ми їх використовуємо. Оскільки проблема атомарності виникає через те, що ми намагаємося імітувати єдине, необхідно знайти таку модель данних у якій є дійсне єдине, а не його імітація. Тобто ключ до відмови від транзакцій знаходиться у архітектурі данних.
Фундамент
Більшість розробників, нажаль, не розуміють що таке дані. Дані – це фундамент на якому будуються інформаційні системи і якщо фундамент поганий, то і вся побудова нікчемна.
Дані – це факти, а факти – це свідчення про події минулого. Події минулого – це те, що вже відболося і не може бути змінено. Якщо свідчення про події змінюються – це недостовірні, погані свідчення на які не можна покладатися та ставити інформаційні побудови, а отже факти також мають бути незмінні.
Ми можемо додавати нові факти до нашої інформаційної системи, або видаляти старі, які нас більше не цікавлять, проте має бути суворо заборонено змінювати самі факти, оскільки такі зміни дають тріщини у фундаменті на якому стоїть уся наша інформаційна споруда, фабрика висновків.
На практиці можна спостерігати, що такого підходу майже ніхто не дотримуєть і замість фактів більшість розробників працюють з цифровими моделями, які намагаються імітувати об’єкти реального світу або тієї чи іншої предметної області. Згадати то же ActiveRecord з Ruby on Rails, де записи у БД – це не факти, а поточні стани певних об’єктів.
У таких випадках навіть розуміння того, що є моделю не вірне. Моделю має бути сукупність фактів та відносини між ними. Тобто, на кожен один додаток чи сервіс, чи ізольований модуль/контекст має буде одна модель даних. Натомість, у Ruby on Rails спільноті (і не тільки) моделями називають окремі класи, що відображаються один до одного на таблиці/реляції у БД.
То ж як позбутися транзакцій?
У прикладі з переказом коштів ми використовуємо не факти, а стани облікових записів. Навіщо?! Замість того, аби оновлювати баланси у облікових записах, ми маємо просто створювати один єдиний факт переказу, який містить у собі наступні поля:
- ідентифікатор облікового запису з якого виконується переказ
- ідентифікатор облікового запису на який виконується переказ
- сума переказу
- дата та час переказу
При цьому варто зазначити, що облікові записи – це і є ідентифікатори які ще можна розглядати як факти про існування чогось унікального. Персональний обліковий запис – це факт про існування людини – користувача. Корпоративний обліковий запис – це факт про існування певної юридичної особи. Відносини між персональними обліковими записами та корпоративними – це також факти, як і припинення таких відносин.
При описаному вище підході зникає необхідність у транзакціях, оскільки запис чогось одного є атомарним. Так, в нас підвищується гранулярність данних та дещо збільшується кількість строк коду, проте ми маємо дуже гнучку, цілісну та зрозумілу модель даних, якої не маємо у випадку, коли працюємо зі станами, а не з даними.
Залишити відповідь