Тема: Анотація Transactional + isolation
Цікавить призначення атрибуту isolation в анотації transactional. Тобто в якому реальному прикладі варто цей атрибут використовувати? Якщо що то на моєму проєкті БД Postgre
Ви не увійшли. Будь ласка, увійдіть або зареєструйтесь.
Ласкаво просимо вас на україномовний форум з програмування, веб-дизайну, SEO та всього пов'язаного з інтернетом та комп'ютерами.
Будемо вдячні, якщо ви поділитись посиланням на Replace.org.ua на інших ресурсах.
Для того щоб створювати теми та надсилати повідомлення вам потрібно Зареєструватись.
Український форум програмістів → Java → Анотація Transactional + isolation
Сторінки 1
Для відправлення відповіді ви повинні увійти або зареєструватися
Цікавить призначення атрибуту isolation в анотації transactional. Тобто в якому реальному прикладі варто цей атрибут використовувати? Якщо що то на моєму проєкті БД Postgre
Цікавить призначення атрибуту isolation в анотації transactional
isolation - можливість вказувати рівень ізольованості для транзакції (оскільки при виконанні декількох транзакцій паралельно, можуть виникати певні проблеми). Тут головне не забувати про деякі моменти (інколи про них дійсно забувають):
- не варто сліпо покладатися на стандарт SQL-92 , в якому визначено чотири рівні ізоляції. Так він є, але кожна СУБД реалізовує дані стандарти по своєму. Й якщо глянути в документацію PostgreSQL то можна помітити наступне:
...In PostgreSQL, you can request any of the four standard transaction isolation levels, but internally only three distinct isolation levels are implemented, i.e., PostgreSQL's Read Uncommitted mode behaves like Read Committed...
- також, кожна СУБД визначає, який рівень ізоляції для них є дефолтним (для прикладу в PostgreSQL - Read Committed, а в MySQL - Repeatable read і тд.).
- варто ознайомитися з документацією конкретної СУБД аби розуміти від яких саме проблем/аномалій буде "захищати" конкретний рівень ізольованості
Тому, перш ніж вказати якесь значення для isolation, потрібно врахувати всі вище згадані моменти.
Тобто в якому реальному прикладі варто цей атрибут використовувати?
Таких випадків може бути багато й вони всі можуть бути досить різні й відповідно потребують різних рівнів ізоляції
Для прикладу можна розглянути варіант з PostgreSQL + дууууже спрощеним варіантом інтернет-магазину.
Єдина таблиця:
- id (ідентифікатор запису)
- number (номер рахунку)
- balance (сума на рахунку)
В даній таблиці зберігаються, як рахунки клієнтів, так й рахунок самого магазину. Для купівлі товарів маємо таку логіку:
1. дізнатися баланс на рахунку клієнта
2. якщо грошей достатньо, то можна далі продовжувати операцію
3. зняти з рахунку клієнта N суму грошей
4. поповнити рахунок магазину на N суму грошей
Тобто три SQL запити мають бути виконані в одній транзакції аби гроші нікуди не зникли. Номер рахунку клієнта, нехай буде 666, а номер рахунку самого магазину - 007 й N=100, тобто клієнт хоче придбати товар саме на таку суму:
-- дізнатися баланс на рахунку клієнта
SELECT balance
FROM credit_cards
WHERE number = '666';
-- в самому коді перевірити чи достатньо коштів на рахунку клієнта
if (balance >= sum)
-- зняти з рахунку клієнта N суму грошей
UPDATE credit_cards
SET balance = balance - 100
WHERE number '666';
-- поповнити рахунок магазину на N суму грошей
UPDATE credit_cards
SET balance = balance + 100
WHERE number '007';
На перший погляд, все виглядає досить норм. Але є певний момент й він досить важливий. А, що може бути, якщо декілька таких транзакцій будуть виконуватись паралельно з рівнем ізоляції Read Committed (тобто дефолтний варіант для PostgreSQL)? Може виникнути аномалія - втрачене оновлення (lost updates) й клієнт зможе "накупити" собі товарів на значно більшу суму (реальні випадки - Flexcoin, Poloniex).
Якщо ж вся вище описана логіка буде виконуватися в єдиній транзакції з рівнем ізольованості - Repeatable read то виникне помилка (Could not serialize access due to concurrent update), яка не дасть можливості юзеру "набрати зайвого товару". Тобто, лише єдина зміна, а саме рівень ізольованості транзакції, все змінює.
Звісно, в цьому випадку, зміна рівня ізольованості транзакції це не єдине вірне/коректне рішення (можна використовувати й дефолтний рівень, але внести зміни в інші місця). Проте, як одне з рішень, цілком підходить саме вказання коректного рівня транзакції.
P.S. Транзакції і Багатопоточний доступ до БД
Read Committed
Repeatable read
можна використовувати й дефолтний рівень, але внести зміни в інші місця
Як саме це реалізується?
Ну варіантів є декілька.
Для прикладу - песимістичне блокування
Тобто використовувати SQL запити - SELECT ... FOR UPDATE + бажано вказати тайм-аут
Або ж глянути вбік оптимістичного блокування:
Клієнт 1 отримує запис під номером 19 із номером версії 1.
Клієнт 2 отримує той самий запис під номером 19 із номером версії 1.
Обидва клієнти роблять зміни над одними і тими самими даними.
Клієнт 2 зберігає свої зміни в сховищі та збільшує номер версії до 2.
Клієнт 1 намагається зберегти свої зміни. Він перевіряє номер версії свого запису із тим що в сховищі та бачить що вони відрізняються. Клієнт може скасувати транзакцію, або ж перевірити наявність конфлікту між даними та спробувати його вирішити.
Тобто, потрібно в таблиці зберігати версію для кожного запису:
- id (ідентифікатор запису)
- number (номер рахунку)
- balance (сума на рахунку)
- version (версія запису)
Відповідно при запиті на оновлення запису:
- збільшувати значення в полі version (+1)
- додавати додаткову фільтрацію по полю version (поточне значення)
UPDATE credit_cards
SET balance = balance - 100, version = 1
WHERE number '666' AND version = 0;
Це й буде запобігати втраті оновлень при паралельних транзакціях.
Якщо ж говорити про Spring Data JPA (Hibernate ) то потрібно додати до сутності нове поле, яке й буде використовуватися в даних цілях:
...
@Version
private short version;
...
Hibernate буде змінювати значення даного поля автоматично. Так само й автоматично буде формувати SQL запити з використанням даного поля при UPDATE/DELETE. Ну й відповідно бути готовим до обробки - ObjectOptimisticLockingFailureException
Якщо немає бажання додавати нове поле в таблицю, то можна "повісити" дану анотацію на поле з міткою часу (звісно, якщо таке є, для прикладу - lastUpdate і тд.) або взагалі обійтися без номера версій та часового поля (OptimisticLockType.ALL/OptimisticLockType.DIRTY). Проте там є багато різних нюансів, тому варто добре подумати перед тим, як відкидати варіант з новим полем, яке зберігає номер версії.
Сторінки 1
Для відправлення відповіді ви повинні увійти або зареєструватися