1

Тема: Анотація Transactional + isolation

Цікавить призначення атрибуту isolation в анотації transactional. Тобто в якому реальному прикладі варто цей атрибут використовувати? Якщо що то на моєму проєкті БД Postgre

Подякували: leofun011

2 Востаннє редагувалося fed_lviv (07.01.2023 17:53:04)

Re: Анотація Transactional + isolation

tan написав:

Цікавить призначення атрибуту 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, потрібно врахувати всі вище згадані моменти.

tan написав:

Тобто в якому реальному прикладі варто цей атрибут використовувати?

Таких випадків може бути багато й вони всі можуть бути досить різні й відповідно потребують різних рівнів ізоляції :)

Для прикладу можна розглянути варіант з 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
https://replace.org.ua/uploads/images/3603/d0e9baea14d1365ffc8077254d03b9ca.gif

Repeatable read
https://replace.org.ua/uploads/images/3603/14394b97b0d3f39d5d497d1601f76c1d.gif

Подякували: tan, leofun012

3

Re: Анотація Transactional + isolation

можна використовувати й дефолтний рівень, але внести зміни в інші місця

Як саме це реалізується?

4

Re: Анотація Transactional + isolation

Ну варіантів є декілька.

Для прикладу - песимістичне блокування
Тобто використовувати 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). Проте там є багато різних нюансів, тому варто добре подумати перед тим, як відкидати варіант з новим полем, яке зберігає номер версії.

Подякували: tan, leofun012