Використання транзакцій в реляційних базах даних: забезпечення надійності та цілісності
Привіт усім! Мене звати Назар, я — Node.js Engineer у компанії Yalantis, працюю на проєкті у сфері IoT.
Тема використання транзакцій в реляційних базах даних є ключовою для розуміння принципів забезпечення цілісності та консистентності даних у системах керування базами даних (СКБД). Розуміння теми дозволяє отримати знання основних принципів та практичного застосування транзакцій в реляційних базах даних, що може мати значний вплив на роботу з даними та розвиток програмного забезпечення.
У цій статті ми розглянемо сутність транзакцій та їхні властивості.
Що таке транзакція
Транзакція у реляційних базах даних — це логічна одиниця роботи, яка об’єднує одну чи кілька операцій з базою даних. Вона має чотири ключові властивості, які визначають її поведінку: атомарність, консистентність, ізоляцію та тривалість (ACID).
У багатьох реляційних базах даних, як-от PostgreSQL, MySQL, Oracle, є можливість використання транзакцій через мову запитів.
SQL
BEGIN TRANSACTION; -- Виконання операцій з базою даних COMMIT; -- Підтвердження транзакції -- АБО ROLLBACK; -- Скасування транзакції
Якщо розглядати транзакції за допомогою різних ORM:
Typeorm
// використовуємо DataSource або EntityManager:
await myDataSource.manager.transaction(async (transactionalEntityManager) => {
await transactionalEntityManager.save(users)
await transactionalEntityManager.save(photos)
// ...
});
// або QueryRunner:
const queryRunner = dataSource.createQueryRunner();
// нова транзакція:
await queryRunner.startTransaction()
try {
// виконати деякі операції над цією транзакцією:
await queryRunner.manager.save(options)
// Фіксуємо транзакцію:
await queryRunner.commitTransaction()
} catch (err) {
// якщо у нас є помилки, відкотимо внесені зміни
await queryRunner.rollbackTransaction()
}
Sequelize
// Спочатку ми запускаємо транзакцію з з'єднання і зберігаємо її в змінну
const transaction = await sequelize.transaction();
try {
// Потім ми робимо декілька викликів, що передають цю транзакцію як опцію:
const user = await User.create({
firstName: 'Bart',
lastName: 'Simpson'
}, { transaction });
await user.addSibling({
firstName: 'Lisa',
lastName: 'Simpson'
}, { transaction });
// Якщо виконання дійшло до цього рядка, то помилок не було.
// Фіксуємо транзакцію.
await transaction.commit();
} catch (error) {
// Якщо виконання дійшло до цього рядка, то виникла помилка.
// Відкочуємо транзакцію.
await transaction.rollback();
}
Управління транзакціями

Команди керування транзакціями
Команди управління транзакціями використовуються тільки з командами DML, як-от — INSERT, UPDATE та DELETE. Вони не можуть використовуватися під час створення таблиць або їх видалення, оскільки ці операції автоматично фіксуються в базі даних.
Команда COMMIT
Команда COMMIT — це транзакційна команда, що використовується для збереження змін, внесених транзакцією в базу даних. Команда COMMIT зберігає всі транзакції в базі даних з моменту виконання останньої команди COMMIT або ROLLBACK.
Синтаксис команди COMMIT такий:
COMMIT;
Нижче наведено приклад, в якому з таблиці будуть видалені клієнти з віком = 25, а потім ці зміни будуть збережені в базі даних.
DELETE FROM CUSTOMERS WHERE AGE = 25; COMMIT;
Команда ROLLBACK
Команда ROLLBACK — це транзакційна команда для скасування транзакцій, які ще не були збережені в базі даних. Ця команда може використовуватися тільки для скасування транзакцій з моменту виконання останньої команди COMMIT або ROLLBACK.
Синтаксис команди ROLLBACK наступний:
ROLLBACK;
Нижче наведено приклад, в якому з бази даних будуть видалені всі записи для яких вік = 25, а потім ці зміни будуть скасовані.
DELETE FROM CUSTOMERS WHERE AGE = 25; ROLLBACK;
Команда SAVEPOINT
SAVEPOINT — це точка транзакції, до якої ви можете повернути транзакцію, не відкочуючи її повністю. Синтаксис команди SAVEPOINT наведено нижче:
SAVEPOINT SAVEPOINT_NAME;
Ця команда призначена тільки для створення SAVEPOINT в інших транзакційних операторах. Команда ROLLBACK використовується для скасування групи транзакцій до точки SAVEPOINT.
Синтаксис, який використовується для повернення до SAVEPOINT:
ROLLBACK TO SAVEPOINT_NAME;
Нижче наведено приклад ситуації, коли ми плануємо видалити три різних записи з таблиці CUSTOMERS. Ми можемо створити SAVEPOINT перед кожним видаленням, щоб мати можливість відкотити операцію SAVEPOINT у будь-який час і повернути дані у вихідний стан.
Наступний блок коду містить ряд операцій:
SAVEPOINT SP1; Savepoint created. DELETE FROM CUSTOMERS WHERE ID=1; 1 row deleted. SAVEPOINT SP2; Savepoint created. DELETE FROM CUSTOMERS WHERE ID=2; 1 row deleted. SAVEPOINT SP3; Savepoint created. DELETE FROM CUSTOMERS WHERE ID=3; 1 row deleted.
Тепер, після того, як були виконані зміни, припустимо, що ви передумали і вирішили відкотити операцію до точки SAVEPOINT, яку ви визначили як SP2. Оскільки SP2 була створена після першого видалення, останні два видалення будуть скасовані:
ROLLBACK TO SP2; Rollback complete.
Команда RELEASE SAVEPOINT
Команда RELEASE SAVEPOINT використовується для видалення створеної точки SAVEPOINT. Синтаксис команди RELEASE SAVEPOINT такий:
RELEASE SAVEPOINT <SAVEPOINT_NAME>;
Після того, як SAVEPOINT буде видалена, ви більше не зможете використовувати команду ROLLBACK для скасування транзакцій, виконаних після останньої SAVEPOINT.
Команда SET TRANSACTION
Команда SET TRANSACTION може використовуватися для ініціювання транзакцій бази даних, а також для визначення характеристик транзакцій, яка задається після команди. Наприклад, ви можете задати для транзакції режим тільки читання або читання і запис. Синтаксис команди SET TRANSACTION такий:
SET TRANSACTION [ READ WRITE | READ ONLY ];
Транзакції: основні концепції
Транзакція у реляційних базах даних — це група операцій, які виконуються як один блок. Вони мають чотири важливі властивості, об’єднані під акронімом ACID:
- Атомарність гарантує, що всі операції в транзакції виконуються повністю або жодна з них не застосовується, запобігаючи неприпустимим проміжним станам даних.
- Консистентність визначає, що база даних переходить з одного стану в інший, що відповідає внутрішнім правилам цілісності, забезпечуючи цілісність даних.
- Ізоляція гарантує, що результати однієї транзакції не впливають на результати інших паралельних транзакцій.
- Тривалість (довготривалість) підтверджує, що зміни, внесені транзакцією, є стійкими та зберігаються в базі даних навіть у разі відмови системи.
Проблема паралельного доступу
Існує така річ, як-от проблема паралельного доступу в системах керування базами даних (СКБД). Виникає, коли кілька транзакцій одночасно звертаються та змінюють одні й ті ж дані. Це може призвести до конфліктів, неправильних результатів чи навіть втрати цілісності даних.
Основні проблеми паралельного доступу:
- Фантомне читання (Phantom Reads): ця проблема виникає, коли транзакція читає набір даних, які вже були змінені чи вставлені іншою транзакцією. Це може призвести до неправильних результатів та неконсистентності відображених даних.
- Неповторювані читання (Non-Repeatable Reads): коли транзакція читає декілька разів одні й ті ж дані, інша транзакція в цей час вносить зміни, що призводить до того, що перше читання відрізняється від наступних, що може призвести до неконсистентності даних.
- Втрати оновлень (Lost Updates): коли дві транзакції одночасно оновлюють одні й ті ж дані, одне з оновлень може бути втрачено через конфлікт. Наприклад, якщо обидві транзакції спробують змінити один рядок у базі, тільки одне оновлення може бути застосоване, а інше — втрачене.
- Аномалії читання-запису (Read-Write Anomalies): це стосується ситуацій, коли транзакція читає дані, які одночасно змінюються іншими транзакціями, що призводить до нестійкості результатів операцій.
Розв’язання проблем паралельного доступу охоплює використання різних рівнів ізоляції транзакцій, де кожен рівень має свої особливості щодо блокування даних для забезпечення консистентності, а також розробку оптимізованих алгоритмів та стратегій доступу до даних для мінімізації конфліктів між транзакціями. Також важливо ретельно проєктувати бази даних та оптимізувати запити до даних для зменшення можливості виникнення конфліктів під час паралельного доступу до них.
Оптимізація використання транзакцій
Оптимізація використання транзакцій в системах керування базами даних є ключовим аспектом для підвищення продуктивності, забезпечення консистентності даних та уникнення конфліктів між транзакціями, а також полягає у виборі найбільш відповідних рівнів ізоляції, використанні розумних стратегій коміту та роллбеку, уникненні зайвих операцій, що можуть уповільнювати транзакції.
Ось кілька підходів до оптимізації використання транзакцій:
- Мінімізація тривалості транзакцій: мінімізувати час, протягом якого транзакція утримує блокування на ресурси бази даних. Це можна зробити, обмежуючи обсяг даних, з якими працює транзакція, та оптимізуючи запити до бази даних.
- Мінімізація блокування ресурсів: використовуйте оптимізовані методи блокування, як-от READ COMMITTED чи REPEATABLE READ, які можуть зменшити конфлікти між транзакціями.
- Оптимізація структури даних: навіть перед використанням транзакцій, потрібно переконатись, що структура даних та індекси оптимізовані для ефективного доступу та операцій.
- Масштабування та реплікація: масштабування системи та використання реплікації для розділення навантаження та підвищення продуктивності.
- Аналіз та моніторинг: аналіз та моніторинг використання транзакцій у системі, щоб виявляти можливі місця для оптимізації та вдосконалення продуктивності.
Кожна система має свої особливості, тому оптимізація використання транзакцій вимагає ретельного аналізу конкретної архітектури та потреб системи для досягнення найкращих результатів.
Рівні ізоляції транзакцій
Рівні ізоляції, як-от READ COMMITTED, REPEATABLE READ, SERIALIZABLE, визначають рівень видимості даних між різними транзакціями. Розуміння цих рівнів є ключовим для запобігання конфліктам та неправильним результатам у паралельних транзакціях.
Read committed — це рівень ізоляції, який надає певний баланс між консистентністю даних і ресурсоємністю системи, забезпечує дві основні гарантії. Перша — жодних брудних операцій читання (Dirty read). Це означає, що під час читання з бази даних клієнт бачить лише зафіксовані дані.
Тобто якісь середні, неузгоджені дані він не може прочитати. І друга гарантія — це жодних брудних операцій запису. Також дозволяє більшій кількості транзакцій працювати паралельно, але потребує уваги до можливості фантомного читання та потенційної нестабільності в деяких умовах. Вибір рівня ізоляції варто розглядати в контексті конкретних вимог вашої системи та важливості стійкості даних.
Основні принципи Read committed:
- Короткий термін блокування: під час виконання Read committed транзакція блокує тільки ті рядки, які читає в поточний момент. Це означає, що інші транзакції можуть змінювати ті ж самі рядки, поки поточна транзакція читає дані, але не можуть внести зміни, поки поточна транзакція не закінчить свою роботу.
- Попереднє блокування для змін: якщо транзакція має намір модифікувати певний рядок, вона заблокує його для змін у поточний момент. Це запобігає конфліктам між транзакціями, які намагаються змінити одні й ті ж рядки даних одночасно.
- Читання поточних даних: транзакція, яка використовує рівень Read committed, читає тільки ті дані, які вже були закомічені (зафіксовані) і не блокуються для змін в момент читання.

REPEATABLE READ — це ще один рівень ізоляції транзакції забезпечує ситуацію, під час якої поточна трансакція завжди бачить дані в тому стані, у якому вони перебували на момент її старту.
Наприклад, якщо в нас є великі запити до яких вкладений величезний об’єм логіки за якої скануються багато таблиць або взагалі вся база даних, і якщо такі запити будуть бачити частину старих даних, а потім додавати частину нових, зрештою ми отримуємо різнобічні дані. У таких варіантах рівень Read committed не підходить, а для запобігання цим проблемам існує рівень ізоляції знімків стану — Snapshot isolation, який ще називають Repeatable read.
В режимі REPEATABLE READ кожна транзакція працює з фіксованою версією даних, відомою як узгоджений знімок (Consistent snapshot) стану бази. Це означає, що коли транзакція починає свою роботу, вона робить «зліпок» всіх даних у базі в певний момент часу і працює із цим набором даних протягом свого життєвого циклу. Навіть якщо під час роботи транзакції інші транзакції внесли зміни в дані, наша транзакція буде працювати з даними так, як вони були на момент її старту, і не побачить цих змін.
Основні принципи REPEATABLE READ:
- Блокування на читання: транзакція, яка працює на рівні REPEATABLE READ, блокує рядки, які вона читає, до завершення всієї транзакції. Це запобігає будь-яким змінам даних, які можуть вплинути на результати читання протягом життєвого циклу транзакції.
- Унікальне читання (non-repeatable reads): оскільки транзакція блокує рядки, які вона читає, це запобігає змінам цих даних з боку інших транзакцій, що гарантує те, що транзакція буде читати одні й ті ж дані протягом усього життєвого циклу.
- Загальне блокування: якщо транзакція виконує операції на декількох рядках, вона блокує всі ці рядки до завершення транзакції, навіть якщо вона читає їх окремо.
- Високий рівень ізоляції: REPEATABLE READ надає високий рівень ізоляції порівняно з «READ COMMITTED», оскільки блокує рядки для всього життєвого циклу транзакції.

SERIALIZABLE — це ще один рівень ізоляції транзакцій у базах даних, який забезпечує найвищий рівень стійкості даних та ізоляції між транзакціями.
Основна ідея SERIALIZABLE полягає в тому, що кожна транзакція працює так, ніби вона одноразово виконується в системі, без будь-яких втручань з боку інших транзакцій. Це досягається за допомогою блокування ресурсів в спосіб, що запобігає будь-яким конфліктам між транзакціями та забезпечує послідовність виконання операцій, як у послідовно виконуваних транзакціях.
Основні принципи SERIALIZABLE:
- Строге блокування: цей рівень ізоляції застосовує найбільш жорстке блокування, що призводить до блокування всіх даних, до яких звертається транзакція, протягом усього її життєвого циклу.
- Запобігання «фантомному читанню» та іншим аномаліям: транзакції на рівні SERIALIZABLE гарантують, що жодні нові рядки даних не з’являються під час їхнього виконання, запобігаючи «фантомному читанню» (Phantom read) та іншим аномаліям.
- Високий рівень консистентності: цей рівень ізоляції дозволяє транзакціям працювати з даними у послідовності, яка відповідає їхньому порядку виконання.

Практичне використання
Приклад 1. Банківська транзакція
Клієнт переказує кошти з одного банківського рахунку на інший. Використання транзакцій в цьому разі є критично важливим, оскільки система повинна гарантувати, що кошти будуть або повністю переведені, або не будуть переведені взагалі. Така атомарність забезпечує цілісність фінансових даних клієнта.
BEGIN TRANSACTION; UPDATE Рахунки SET Баланс = Баланс - 100 WHERE ID = 123; -- Зняти гроші з рахунку UPDATE Рахунки SET Баланс = Баланс + 100 WHERE ID = 456; -- Переказати гроші на інший рахунок COMMIT; -- Підтвердження транзакції -- АБО ROLLBACK; -- Скасування транзакції у випадку помилки
Приклад 2. Масове оновлення користувачів з неактивними підписками
Наприклад, нам потрібно раз в день стягувати підписки з пеймент-сервісу та оновлювати в базі даних, для цього підійде cron job і звичайна логіка, яка буде це оновлювати. Через те, що в нас може бути багато записів з неактивними підписками, і під час оновлення великої кількості даних може статися щось непередбачуване, транзакції стануть надійним засобом для запобігання неправильним даним чи надлишковим артефактам у базі даних.
async updateExpiredSubscriptions(): Promise<void> {
const expiredSubscriptions = await SubscriptionService.getExpiredSubscriptions();
if (expiredSubscriptions.length) {
const transaction = await db.connector.transaction();
try {
await Promise.all(
expiredSubscriptions.map((expiredSubscription) =>
expiredSubscription.update({ status: 'expired' }, { transaction }),
),
);
await transaction.commit();
} catch (err) {
await transaction.rollback();
}
}
}
Висновок
Транзакції у базах даних є невід’ємною складовою для забезпечення надійності та цілісності даних. Розуміння їхніх принципів та правильне використання допомагають підвищити ефективність та надійність системи в будь-якій сфері. Використання транзакцій в системах керування базами даних має свої переваги та недоліки, які варто враховувати під час проєктування та розробки програмного забезпечення.
Переваги використання транзакцій:
- Забезпечення консистентності даних: транзакції дозволяють гарантувати, що зміни в базі даних відбуваються відповідно до визначених правил, забезпечуючи консистентність і цілісність даних.
- Атомарність операцій: транзакції забезпечують атомарність, тобто операції виконуються або в повному обсязі, або не виконуються взагалі. Це запобігає втраті чи неправильним станам даних у разі помилок.
- Ізоляція та паралельність: транзакційна ізоляція дозволяє кільком транзакціям працювати паралельно без взаємовпливу, що підвищує продуктивність та швидкодію системи.
- Відновлення в разі відмови: транзакційні журнали та механізми анулювання дозволяють відновлювати стан бази даних до попередніх точок часу в разі виникнення помилок або відмов системи.
Недоліки використання транзакцій:
- Блокування та конфлікти: часте використання транзакцій та блокування ресурсів може призводити до конфліктів та затримок у роботі системи.
- Великий обсяг пам’яті: транзакційні системи можуть вимагати значних обсягів пам’яті та обчислювальних ресурсів для підтримки ізоляції та відновлення даних.
- Накладні витрати на управління: підтримка транзакцій вимагає складних механізмів управління та журналізації, що може призводити до додаткових накладних витрат.
- Можливість втрати продуктивності: неправильне використання транзакцій може призводити до втрати продуктивності через зайві блокування та обробку даних.
А також моніторинг та аудит транзакцій є важливими для виявлення проблем та відстеження змін у базі даних. Сюди входять відстеження виконання транзакцій, аналіз помилок та управління журналами транзакцій. Транзакції у базах даних є невід’ємною складовою для забезпечення надійності та цілісності даних. Розуміння їхніх принципів та правильне використання допомагають підвищити ефективність та надійність системи в будь-якій сфері.
75 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівДякую за пост, але дуже багато недоліків і неточностей
тривалість — це duration, а durability — це витривалість
Ви вже визначитеся нарешті, як правильно перекласти слово manage
А чому не впливають?
Якщо рівень ізоляції READ UNCOMMITTED, одна транзакція може бачити результати роботи інших транзакцій
Цікаво, а як можна перевірити, чи індекси оптимізовані для виконання транзакцій?
Написал список замечаний и стер....
Проще сказать что для тех кто занимается rdbms вплотную — лучше не читать. Тем более фокус на MS SQL only. Автор должен делать соответствующий дисклеймер, дабы не вводить в оману.
ЗЫ за перевод на Украинский спасибо
— приїхали! Які це недоліки? Так, це накладні витрати на ЗАБЕСПЕЧЕННЯ ТИХ ПЕРЕВАГ, ЯКІ НАДАЮТЬ ТРАНЗАКЦІЙНІ БД.
Проблема паралельного доступу.
Насправді проблеми, згадані у розділі, — дещо вигадані проблеми. Розглядаються теоретично можливі варіанти. Але дійсно у книгах деяких авторів увага на це не акцентується. В більшості сучасних провідних СКДБ таких проблем не існує. Хоча фантомне читання було у ранніх версіях MS Sql при деяких налаштуваннях параметрів.
Яких проблем не існує в сучасних провідних СКДБ? Не зрозумів з контексту, розгорніть, будь ласка
Можливо дещо пербільшив у попередньому повідомленні
- треба розуміти, що ви читаєте дані, якими вони були на початок виконання оператора або транзакції
— залежить від рівня ізоляції.
— якщо якась з транзакцій не може бути виконана через конфлікт з іншою транзакцією, то вона завершиться помилкою. Просто треба після кожного звернення до бд перевіряти статус завершення операції.
— те саме що і з Фантомним читанням. Просто треба добре розуміти, які дані або їх зміни бачить транзакція\окремий оператор, а які не бачить.
Але якщо логіка (процедури\функції ) розроблена на боці бд, то розробників клієнтських додатків, які свою розробку виконують на рівні виклику функцій бд, ці проблеми не стосуються. Достатньо, що їх розуміють розромники бд.
— мається на увазі, що читалися вставлені\змінені дані ще не зафіксованих інших транзакцій. Саме такого роду проблем, коли читаються дані незафіксованих транзакцій, у сучасних СКБД немає
Трохи є. По різному можна зрозуміти, що проблем нема, типу, і транзакції вже менеджити не треба, тому і перепитав.
Га? Тобто, що мається на увазі?
А ящо нема конфлікту? Другий приклад з статті — це якраз про Lost Updates.
Це не Phantom Read, а Dirty Read. Я з MS Sql не працював, але здається, так, він дозволяє виставляти Read Uncommitted.
На всі питання не відповім. Варіантів безліч. Але наголошую — це не проблема. Просто треба добре знати інструмент, яким користуєшся, як він працює. Справжня проблема в тому, що не всі знають, як працює інструмент, і не хочуть в цьому розбиратися. І це стосується будь якого інструменту, не тільки БД.
Щодо
:
Ваш Select почав виконуватись о 10:00, він може виконуватись мілісекунди, або хвилини, або навіть години. У будь якому разі за цей час інші транзакції можуть змінити скільки завгодно даних (таблиць\рядків\окремих атрибутів), у тому числі тих, які відповідають умовам вашого Select. Але ваш Select не бачить цих змін, він бачить дані станом на 10:00. Я сподівався, що хоча б це розуміють всі, хто використовує транзакційні бд.
Знову не зрозумів. Що — не проблема?
Усе вказане в розділі
статті.
Давайте ще раз.
Це як — в сучасних СКБД (в Oracle, наприклад) — вже не проблеми? Пофіксили? ))
Ці проблеми тільки
були?
Ще раз. Я наголошую, що це не проблеми БД, це особливості її роботи, про які треба знати. І які має враховувати розробник, якщо це створює ПРОБЛЕМИ ДЛЯ НЬОГО. Деякі з цих ПРОБЛЕМ РОЗРОБНИКА можна усунути обравши відповідний рівень ізоляції транзакцій, або застосувавши блокування. Або питання може бути вирішине на адміністративному рівні — забороняємо доступ к БД з правом вносити зміни на час виконання критичних звітів. Або будуємо критичні звіти на резервному сервері, на якому відключаємо оновлення на час побудови — і це реальне рішення, яке використовувалось у моїй практиці (окремий сервер для побудови складних ресурсоємних звітів)
Ну, в такому формулюванні згоден, звісно.
А то ви пишите
... можна по різному зрозуміти.
От автор, повторюсь, ігнорує Lost Updates у своєму другому прикладі
тому що, мабуть, не розуміє, що саме копі-пастить з ChatGPT.А так-то у нас проблем немає, так. Просто знати різні особливості та код без багів писати, ось і все.
1. Щодо DDL і транзакцій, то твердження хибне. Залежить від БД. Для Oracle це так — кожна DDL операція виконується в окремій транзакції, тобто виконується неявний COMMIT не тільки після, але і ДО операції. Для Postgresql DDL виконується як частина транзакції нарівні з DML.
2. SELECT + SET TRANSACTION — непоодинокі випадки необхідності їх одночасного використання, наприклад для забезпечення узгодженності даних при побудові комлексних звітів, коли виконується більще ніж один Select (явний чи неявний)
3. До команд керування транзакціями треба додати ще RAISE EXCEPTION — завдяки якій можна відкотити тільки частину транзакції. Наприклад це корисно коли ви виконуєте якусь обробку у циклі і для якихось з ітерацій при обробці виникає помилка.
Хм, не знав, і навіть не спадало на гадку таке робити. Не те, щоб одразу практичне застосування придумав, але в будь-якому випадку дякую
Наочний приклад — temporary tables. В Oracle вони створюються заздалегідь, а у Postgresql можуть бути створені на льоту, хоча з таким способом можуть бути пов’язані інші проблеми.
У коментарях майже все вже розібрали, али додам свої 5 копійок.
1. Вибачте, Назар, стаття не виглядає як «так болить, що не можу не писати, буду сперечатись з кожним зауваженням». Вангую, що автора ми в коментарях взагалі не побачимо. Невже роботодавець вимагає ще і написання курсових?
2. Ну, дякуючи ChatGPT, тепер це легко для автора, але тяжко для читачів, оці всі «основні теми, ключові принципи, значне застосування і практичний вплив» з першого абзацу. Це питання до редакції ДОУ (та й всього світу), що з чим робити.
3. Про «команду SAVEPOINT» це з власного досвіду? Чи copy-paste з ChatGPT? Схоже на риторичне питання.
4. Шановні колеги, треба щось робити з Read Skew. Я вже не перший раз чую думку (у статті це словами написано, а другим прикладом показано), що транзакції — це про модифікації, а декілька селектів перед транзакцією — це норм. Ось є коментар, або ось людина інтуітивно відчуває проблему, а її натомість цькують.
5. Про достатній баланс рахунку з першого приклада вже декілька разів згадали.
6. У статті згадуються Lost Updates, а другий приклад — це якраз їх наочна ілюстрація.
Якщо спочатку прочитати
SubscriptionService.getExpiredSubscriptions(), потім у паралельній транзакції відбудеться якась активація, то перша транзакція все одно виставитьexpiredSubscription.update({ status: 'expired' }. Це і є lost update.7. Трохи офтопік. Можна для незнайомих з нодою викликати, будь ласка, невелику пояснювальну бригаду?
Що тут відбувається — простими словами? У ноди по дефолту неблокуючі драйвери до БД? Навіть якщо так, транзакція ж одна? Як працює та який виграш дає Promise.all у цьому прикладі?
Дякую за статтю.
Чат жпт гарно прочитав інструкції до ms sql
Нове дно вайтішніків з орм
дуже стандартний матеріал ну типу як перекладена вікі
а хотілось б чогось цікавенького з власного досвіду може? наприклад як дебажіти локи,
як робити дійсно банківську транзакцію (100% це буде аудит трейл або якісь write ahead лог)
як інкорпоріровать сайд ефекти в транзакцію типа ви по http куда то сходили а транзакція відкотилась, то як далі зробити реконсиляцію,
є така штука як розподілений коміт, що я пару років на роботі робила щось схоже
або чогось металевого, типу як зробити власни транзакції на базі яка їх не підтримує
del
Теорія це завжди добре. Але практика є практика.
Ось у прикладах
1. Банківська транзакція.
Цікавіше було б розглянути кейс коли потрібно спочатку отримати баланс користувача, якусь логіку виконати від якої залежить сума, і потім апдейтити. Наприклад, якщо баланс більше 1000, то до суми додати ще 1.5% (робити це не в базі, а в application). І що буде коли одночасно прийдуть декілька запитів/транзакцій по одному аккаунту. Тому що просто begin/commit не вирішують всі питання.
2. Масове оновлення.
В теорії все так, а на практиці коли є конкурентні апдейти цієї таблиці іншими потоками, то верогідність отримати dead lock, ну дуже висока.
Щодо першого питання. Для цього і створюються мови бд, які включають різні спеціальні конструкції, як-то блокування таблиць/рядків або тригери. І такі речі мають робитися саме у коді на боці бд. Нажаль багато розробників клієнтських додотків, навіть з великим досвідом, цього не розуміють. Як не розуміють керівники проектів, в яких використовуються бд, навіщо їм виокремлені розробники бд, якщо в них є досвідчені розробники клієнтських додатків, які до того ж добре володіють різними ORM
Або, якщо розробка на стороні БД категорично не сприймається, або використовується БД, яка не надає можливості розробки власної логіки, то бачу такі варіанти:
А. Протягом операції забеспечується з’єднання з бд:
begin
select ... for update\lock — блокування якогось ресурсу, який відноситься до кліента, наприклад того ж баланса. І фінансова операція починається тільки після успішного блокування.
commit
Б.Протягом операції немає можливості забеспечити постійне з’єднання з бд, виконуємо операцію покроково
1. Переводимо операцію у статус «готовий до зміни балансу» — це може бути реалізовано як зміна значення якогось з атрибутів клієнта, або Insert в якусь окрему таблицю з Unique constraint по ID клієнта (+ID менеджера+відмітка часу)
2. Виконання (монопольне) фінансової операції
3. Завершення операції (відміна статусу «готовий до зміни балансу»)
4. На випадок якщо вирогідність незавершення операції є високою, то або автоматична відміна статусу при перевищенні певного таймауту, або режим суперменеджера для відміни статусу вручну
А потім будете питати а чого це так БД повільно працює?! А кто ето сделаль? А чого в аналітични вигрузках або немає даних або вони робляться по 6 годин.
Загалом так робить конєшно можна, але це або хеловорлд-левел, або треба мати окремі таблиці для блокувань
Подорожнік, підпертий костильом, з пластирем на якому написали «не ламайсь»
Доросле рішення — distributed fenced lock.
Оце і є типове ставлення до БД розробників клієнтських додатків — головне щоб БД не була гальмом, а цілістність і консистентність даних — а що воно взагалі таке?
— До чого тут взагалі distributed? distributed — це коли мова йде про забеспечення транзакційності на рівні одночасно кількох БД. І по друге — нативні засоби БД забеспечення транзакцій завжди будуть працювати швидше і надійніше. Більше того, передбачаю що різні сторонні рішення так чи інакше працюють через використання нативних засобів БД.
До того що абсолютна більшість софту крутиться зі скейл>1 і по факту стає розподіленою системою.
Ніт. Це коли є конкурентна модифікація одних і тих же даних навіть в 1 БД, яка може прийти з фізично різних машин.
Невірне передбачення )
Ну таке, я не сперечаюсь що прошарок людей які не задумуються над цим є. Але це до першого адського дебага «а чому так сталось».
— якщо мова про 1 БД, то йомовірно клієнтські додатки мають тільки одне з’єднання з БД, а скільки фізичних серверів складають цю БД, для клієнта не має ніякого значення (в більшості випадків він навіть не знає про це), і тоді підтримка distributed транзакцій використовується, але на боці самої БД. Принаймі я жодного разу не стикався з тим, що використовуються окремі з’їднання до різних вузлів/серверів однієї БД і тим більше одночасно.
Ти взагалі не зрозумів, про що я написав.
Мова йде про скейл>1 клієнта.
А мені чомусь здається, що це ти не розумієшь, про що пишеш.
— це про конкурентний доступ. А
— це про забеспечення транзакцій, які одночасно охоплюють кілька різних БД. Це перпендикулярні речі.
Це ти собі якусь персональну термінологію видумав
Ну або ти на все дивишся з точки зору database only. Якщо забути про те, що навколо бд є інший світ, і жити виключно всередині бд, то так, тоді distributed буде так само про скейл>1 тільки про інстанси бд.
Я ж не живу у бд, тому цей термін має інше значення.
Так більшість розробників взагалі не відчувають що якась проблема може бути. І треба робити хоч щось в такому кейсі. Вважають що begin/commit магічним образом вирішують всі проблеми.
А як select ... for update буде афектити якісь «звичайні селекти» щоб тормозити аналітичні вигрузки?
В Oracle та Postgresql блокування рядків точно на Select не впливає. В якихось інших бд можливо впливає.
Так і в mysql також, тому і дивуюсь чого б це аналітичні запити, якім не потрібні блокування, почали тормозити від .. for update
У ранніх версіях MS Sql при певних конфігураціях навіть Select призводив до блокування, так що іншим sql операторам, які звертались до тих же даних, доводилося чекати завершення його виконання.
Не використовував (або вже забув що використовував ))) )SELECT FOR UPDATE на практиці, коли потрібно як раз Distributed Locks використовуємо щоб не тримати конекшн відкритим, але як на мене трохи дивна поведінка. Припустимо у мене є таблиця с бонусами. Id, UserId, BonusAmount, BonusDateTime. З одного інстансу сервісу я відкриваю транзакцію і роблю SELECT FOR UPDATE по Id, тобто блокую одну строку для того щоб збільшити Amount на деякий процент. З іншого інстансу сервісу мені потрібно при наданні нового бонусу перевірити що цього дня по UserId ще не було бонусів. Тобто роблю SELECT по UserId + BonusDateTime. Але ж друга транзакція не знає що перша буде саме Amount змінювати а не UserId чи BonusDateTime. Тобто якщо друга транзакція робить SELECT не за PK (Id) а за комбінацією полів та рівень ізоляції вище ніж READ UNCOMMITTED я б скоріш очікував що вона повинна дочекатися зняття локу на UPDATE.
Хєх, але перевірив, таки так, в MySQL другий SELECT дійсно повертає існуючі дані без очікування навіть з WHERE не за PK, доки його в транзакцію аж з рівнем SERIALIZABLE не обгорнути.
Отут ступаю на тонкий лід, бо сам я не дба, в кишках сучасниз бд не розбираюсь, і лише оперую чужим досвідом у цій сфері.
Я консультутвався з кількома дба в світерах з приводу ...for update. Всі опитані незалежно один від одного підтвердили що в великих кількостях при великій інтенсивності блокувань ...for update може викликати блокування окремих пейджів бд і викликати очікування при селектах. Всі також підтвердили що при високоінтенсивному використанні блокувань їх краще взагалі виносити за межі БД. Один додав що в інфраструктурі SAP є окремий сервіс для блокувань спеціально зроблений з метою рознесення БД і ресурсів що блокуються.
Якось так.
Виходячи з ідеології рознесення відповідальності для себе я вирішив що кращим сценарієм є розподілені локи винесені поза межі БД, і з тих пір успішно з ними працюю.
Не існує сферичної БД у вакуумі. Це завжди якась конкретна БД. У різних БД блокування реалізуються по-різному.
— це називається ескалацією блокування і використовується в тих БД, де для блокування використовуються структури у дефіцитній оперативній пам’яті. В такому разі при певній кількості заблокованих рядків на одній сторінці блокування цілої сторінки обходится дешевше ніж блокування окреміх рядків. В Oracle і Postgresql реалізовано інший механізм підтримки блокування, без використання оперативної пам’яті. Ознака блокування рядка зберігається разом з самими даними на носію даних.
Іноді дійсно є потреба робити блокування не прив’язане до конкретних рядків в базі, але для цього далеко не завжди має сенс використовувати додаткову систему. В сучасних базах є такі механізми блокування: advisory lock в PostgreSQL (www.postgresql.org/...cking.html#ADVISORY-LOCKS), application lock в SQL Server (learn.microsoft.com/...sql?view=sql-server-ver16), lock functions в MySQL (dev.mysql.com/...en/locking-functions.html), і т.д.
Те що вам сказали «дба в світерах» скоріш про lock escalation, який існує не в усіх базах (в тому ж Oracle та PostgreSQL його немає), і в першу чергу він полягає саме в тому, що ви написали — замість того, щоб блочити велику кількість окремих «маленьких» об’єктів бази, блочиться більш високорівневий. Але ця поведінка нічого не каже про сумісність лока на оновлення і звичайного лока на читання на тому самому об’єкті. Ну і хай lock escalation працює, якщо мої селекти без UPDLOCK сумісні з цим блокуванням — то навіть якщо UPDLOCK буде на всій таблиці стояти, нічого не зміниться в поведінці. Треба ще подивитись документацію SQL Server, але елементарний тест з двома SERIALIZABLE транзакціями, які вибирають той самий рядок, але з різним рівнем блокування, показує що обидва читання один одному не заважають.
Нажаль, в стандартних реалізаціях від СУБД не вистачає динамічного таймауту. Тобто, є деякий процес, операція над об’єктом, тощо, який має виконуватись одночасно лише на одному із багатьох інстансів сервісу. Але час виконання може бути як 1 секунда так і десятки хвилин в залежності від обставин. Тобто фіксований таймаут не дуже підходить. Доки інстанс «живий» і продовжує виконувати операцію, блокування повиненне утримуватись. Але якщо інстанс жорстко крешнувся (з тих чи інших причин) потенційно finaly де знімається лок, може не відпрацювати. Тому безкінечний таймаут також не підходить, бо можна отримати вічне блокування. Один із способів вирішення цієї проблеми, це вимірювання таймауту від останнього heartbeat-у. Але ж для відправки heartbeat-ів так чи інакше по перше потрібна SP / функція яка їх приймає, по друге обв’язка у коді яка їх у фоні відправляє.
за більше ніж 20 років роботи з Oracle+Postgresql стикався з таким лише1-го разу. Лок знявся тільки після рестарту бд.
А нащо динамічний таймаут, якщо час життя локів прив’язаний щонайменше до сесії? Впала сесія — вбився лок. В документації того ж MySQL написано:
А в MS SQL та PostgreSQL є ще додатковий рівень гнучкості — такі явні локи можна прив’язувати до транзакції. Впаде транзакція з будь-якої причини — пропаде блокування. В інших базах даних може бути щось схоже, треба дивитися.
Але ж у такому разі потрібно тримати відкритий конекшн протягом усього процесу. Якщо взаємодія з базою займає невеликий процент часу, 2% часу читання, 96% часу обробка, взаємодія з іншими сервісами, та 2% часу оновлення, то навіщо тримати конекшн? Кількість конекшенів все ж таки обмежений ресурс. До речі не звернув увагу на це, це як на мене ще один з мінусів ціх стандартних реалізацій.
Ви підводите під якийсь дуже екзотичний чи штучний кейс :) В світі де намагаються зменшити час ексклюзивного блокування записів ви кажете що є потреба заблокувати щось на такий тривалий час, що навіть час життя конекшену до бази може бути проблемою. Наведіть приклад, будь ласка.
І якщо навіть абстрагуватись від цього... Є два варіанти — або використовувати вбудовані засоби, які дуже прості у використанні і покривають 90%, або будувати «космічний корабель» з додатковими сервісами, харт-бітами та іншою магією. Складність альтернативного рішення повинна бути виправдана вагомими причинами специфічними для конкретного випадку. А відразу будувати щось складне тільки тому що «ну там обробка може зайняти X відсотків часу бізнес-транзакції» — дуже сумнівний підхід.
Я скоріш не про ексклюзивне блокування в сенсі БД, а про логічне блокування бізнес операцій над одним і тим самим об’єктом та деяких операцій в цілому при горизонтальному масштабуванні.
Приклад 1.2-3 секунди. До завершення операції сабміту інші бізнес операції з об’єктом не дозволені. Команда на виконання паралельної операції може бути закинута лоад балансером на любий інстанс сервісу. Чи ефективно буде тримати відкритим конекшн 5-6 секунд тільки заради блокування якщо таких запитів тисячи в секунду?
Банківська транзакція. Читання з бази — міллісекунди, оновлення в базі міллісекунди, для сабміту на 3rd party потрібна взаємодія з декількома сервісами з SLA
Приклад 2.6-10 інстансів сервісу. Кожна окрема група об’єктів за деяким критерієм може оброблятись на будь якому інстансі, але одночасно тільки на одному. Жорстка прив’язка групи до інстансу не бажана, число інстансів динамічне. Тут взагалі не про рядки у базі а про логічне блокування на групу для бізнес операції.
Умовний батчінг процес 10 секунд — 2 хвилини. В кластері
Звісно що існує багато інших способів вирішення, але розподілені логічні блокування це один із них.
В першому випадку ви відразу припускаєте, що обробка бізнес-транзакції розбита на шматки, які виконуються зовсім окремо. Якщо в цьому була реальна потреба з якихось міркувань, то окрема система розподіленого блокування дійсно може бути потрібна. Але, знову таки, будь-які ускладнення повинні бути виправдані — чи дійсно система очікує таке навантаження де без розбивки транзакції на окремі фрагменти ніяк і т.д.
Другий випадок мені самому доводилося будувати, причому з батчами які і години можуть виконуватись. При очікуваному навантаженні і тих бізнес умовах які були, ми могли обмежити одночасне виконання батчів декількома сотнями, а ті що не влазять — ставити в чергу. Якраз використувували advisory lock для блокування. Декілька сотень конекшенів для PostgreSQL — це не проблема. До того ж, в залежності від внутрішньої реалізації батчів, вам може знадобитися дочитування даних в процесі обробки, а тут без живого конекшену буде дуже складно.
Основна думка, яку я намагаюсь донести — не треба переускладнювати рішення, поки на це немає реальної потреби. Так, окремі системи для реалізації розподіленого локу можуть знадобитися, але це не значить, що при появі явного локування в вашому рішенні треба відразу їх додавати. Я вже зустрічав пару схожих випадків — один раз розробник просто не знав про вбудовані можливості сучасних баз даних, а в другому випадку знав, але сумнівався, що їх буде достатньо. Проговорили сценарії і виявилося, що все-таки достатньо.
А отут є нюанс.
Так вже сталось, що в більшостях систем потрібен або кеш або швидка у доступі ін-меморі-помийка для даних, які можна швидко відновити читанням. В більшості випадків це редіс, рідше, в джава світі хазлкаст. І в них розподілені локи йдуть з коробки, без написання окремих вендор-залежних запитів до функцій конкретної субд.
А ще, їх коннект з локом набагато дешевший ніж коннект до субд, який зазвичай обмежений пулом, у, наприклад 200 коннектів, ще й з таймаутом.
І тут є декілька нюансів :)
— Я б не сказав що у переважної більшості проектів є централізований кеш. Так, це часте явище, але в багатьої проектах він не потрібен з різних причин.
— Досить часто треба щось залочити тільки на час виконання транзакції. Тобто ви і так знаєте, що будете тримати конекшен. Нащо тоді ходити в сторонню систему?
— База сама менеджить життєвий цикл локів, які вона надає. Якщо лок прив’язаний до сесії або транзакції — база сама його зніме, коли відповідний ресурс закінчить своє існування. А для зовнішніх локів треба самому менеджити їх життєвий цикл, і відповідно є ймовірність ловити «вічні» локи, поки механізм не буде нормально відлагоджений, тобто імплементація стає складнішою.
— Пул конфігурується, сучасні БД нормально можуть обробляти тисячі конекшенів.
Ще раз повторюсь, що я НЕ кажу, що зовнішні системи розподіленого блокування непотрібні. Рішення треба обирати зваживши всі фактори і вимоги. Може виявитися, що в реальності системою будуть користуватися не більше декількох сотень користувачів одночасно, а ми її будуємо в припущенні що «ми — Гугл» :) До речі, дуже рекомендую статтю — blog.bradfieldcs.com/...e-not-google-84912cf44afb
Дуже слушні приклади
Скоріше не dedlock a просто lock вбо wait
Саме дедлок, коли ще буде якась транзакція, яка оновлює декілька рядків цієї таблиці. Не 100% що так станеться, але чим більша конкурентність, тим більше верогідність цього.
Варто було хоч разок згадати про optimistic locking та розподілені транзакції
Стаття годна, на добре потяне
Колись я думав що все в світі перетвориться на джаваскріпт
Тепер я розумію, що все в світі перетворюється на джаву :)
статус транзакції — текстове поле. На добре не тяне таки
Три з плюсом
а ну, а ну, шо не так з текстовим полем? чи міс’є топить за енами?
А не жирно в пам’ять грузить 100500 таких? Він навіть ні батч не використав ні пейджинг, що ж це буде як chatgpt це побачить?
Таки мсьє топить за енами
Все в світі спочатку стає джаваскріптом, який еволюціонуючи, перетворються на джаву )
Так точніше )
REPEATABLE READ не завжди значить більше блокування — дивіться на його реалізацію в PostgreSQL, або на SNAPSHOT ізоляцію в SQL Server.
Але ж мова про мс sql і RR
Я не бачу де в статті вказано, що вона саме про SQL Server. Стаття взагалі про «сферичні транзакції в вакумі», мабуть мається на увазі ANSI стандарт. Єдиний рядок де згадуються конкретні технології:
, а коли мова йде про Repeatable read, то тут все змішалося:
В тому ж SQL Server це два різних режими ізоляції...
Пару разів відкривав новий проєкт й бачив картину:
А що не так, коли все це в рамках одного коннекту к DB?
І що значить «запит без транзакції»? В mysql є запити які примусово закривають транзакції (Implicit Commit) і людина може про це не знати, тоді розумію про що мова.
Мабуть, хтось знає про Read Skew
Якось занадто поверхнево.
> Команди управління транзакціями використовуються тільки з командами DML, як-от — INSERT, UPDATE та DELETE. Вони не можуть використовуватися під час створення таблиць або їх видалення, оскільки ці операції автоматично фіксуються в базі даних.
Вірно для більшости, але не для всіх баз.
> Ця команда може використовуватися тільки для скасування транзакцій з моменту виконання останньої команди COMMIT або ROLLBACK.
Не згадан autocommit-режим, і що він може бути за замовчуванням. Тоді ви нічого вже не скасуєте.
> SAVEPOINT — це точка транзакції, до якої ви можете повернути транзакцію, не відкочуючи її повністю.
Багато де не реалізовано.
> Приклад 1. Банківська транзакція
Ані слова про те, що будь-який з UPDATE може зависнути на визначений час чи одразу відмовити, що COMMIT може відмовити, що робити у такому випадку. (Я вже не згадую, що реальна банківська транзакція пише ще два десятки таблиць логів і статистики, і скоріш за все сам переказ на цільовий рахунок теж у таблиці тимчасового логу, а не одразу.)
> Основні проблеми паралельного доступу:
І ані слова про те, що цей аналіз дуже поверхневий. Забуті такі проблеми, як read skew, write skew. Щоб почитати, наприклад:
www.cockroachlabs.com/...le/demo-serializable.html
justinjaffray.com/...oes-write-skew-look-like
A Critique of ANSI SQL Isolation Levels, Jun 1995, Microsoft Research (1995! це ж коли було і мало хто знає)
Кожен хто претендує на звання хоча б інтерна з DBA чи мідла у суміжних областях — має це знати і уважати. В такій статті як ця — якщо не в основному тексті, то хоча б у PS треба було б зауважити.
Ні слова про те, як можна керувати рівнем оптимістичности в транзакціях, які рушії (движки?) як з цим працюють (блоковочники і версіонники (MVCC) дуже по-різному відносяться до цього), про переваги всяких select for update де вони є...
Ні слова про те, як застосунок має кешувати дані і як це робиться через різні ORM, навіть не згадане їх існування...
Привіт автору від Даннінга з Крюгером, даруйте.
Якщо висвітлити всі зазначені Вами аспекти, то вийде товстенька книжка. А ця стаття для трейні.
Ні.1-го класу, а IT для дорослих.
По-перше, спрощення не має викидати принципове. Можна писати прості приклади, але завжди пояснювати, хоча б в двух словах, що тут спрощення, а реальна складність буде далі.
Це не математика для
По-друге, для трейні треба писати інакше — щонайменше, починати без асинхронного коду. Тільки коли основи і роботи з БД, і промісів засвоєні — поєднувати їх.
Напиши продовження, я почитаю і гарний коментар залишу! :)
Ти забагато хочеш від джензі. Дай людям бути собою і пробач їх. Стаття на рівні одної університетської лекції бд, яку хтось освоїв самотужки без препода, ну це якщо приколупуватись уже. Not great, not terrible. Нормально.
Якщо бути таким як ти, то доведеться в кожного кинути книжку ульмана, а потім ще шльопнути по губам якимось томіком посучасніше
«Джензі» це хто?
Ні! Ні, і я не про це!
Не треба нам ахо-ульманів, це вигін в зовсім іншу сторону. Навпаки, те, що бачимо у цій статті, це коли спочатку спохаблять, а потім намагаються викласти з мордою академічною цеглою.
Розповідь має бути такою, що легко читається, без занудства, пояснювати для конкретного рівня, але не втрачати принципових моментів.
Тут не виконано ніщо з цього.
Gen-Z
Ну це ж доу, тут не шевченки й не тичини ))
Мені колись якийсь препод ще в школі сказав що щоб пояснювати щось комусь простими словами треба знати тему в 10 раз глибше
Ну так.
Я на це так дивлюсь, в уні не вчать, чешуть яйця, уні обсирають, потім років через 5 відкривають якусь копєєчну статтю в неті, для них відкривається третє око, і вони себе уявляють дуже прошареними :)
Тут ще щось було недавно про індекси
Хай краще вже так ніж ніяк. З часом буде рівень підвищуватись. Я сподіваюсь. Або не буде :)
Взагалі автор не пише і не підвисає. Здається це була стаття для річного перформанс ревю
Для співбесіди хватить)
Співбесіда з відповідями рівня слизького індуса
Чомусь забувають що перед змінами, буде перевірка Баланс на наявність необхідної суми.
Але ок, це питання для співбесіди — а тепер додайте у наведений код таку перевірку :)
Та в жодному банку взагалі таким чином транзакції не відбуваються :)
Це абстрактний приклад. Але питання дуже правильне і багато хто не розуміє про що річ і яка проблема (тому що не думають про конкурентний доступ).