СУБД InterSystems IRIS підтримує цікаві структури для зберігання даних – глобали. По суті, це багаторівневі ключі з різними додатковими плюшками у вигляді транзакцій, швидких функцій для обходу дерев даних, блокувань та своєї мови ObjectScript.
Докладніше про глобали у циклі статей «Глобали — мечі-кладенцы для зберігання даних»:
Мені стало цікаво, як реалізовані транзакції в глобалах, які там є особливості. Адже це зовсім інша структура для зберігання даних, аніж усім звичні таблиці. Набагато більш низькорівнева.
Як відомо з теорії реляційних баз даних, хороша реалізація транзакцій повинна задовольняти вимогам :
A - Atomic (атомарність). Записуються всі зміни, зроблені в транзакції або взагалі ніяких.
З - Consistency (узгодженість). Після завершення транзакції логічний стан БД має бути внутрішньо несуперечливим. Багато в чому ця вимога стосується програміста, але у випадку SQL-баз даних вона стосується також зовнішніх ключів.
I - Isolate (ізольованість). Транзакції, що паралельно виконуються, не повинні впливати один на одного.
D - Durable (довговічність). Після успішного завершення транзакції проблеми на нижніх рівнях (збій по живленню, наприклад) не повинні впливати на дані, змінені транзакцією.
Глобали – це нереляційні структури даних. Вони створювалися для надшвидкої роботи дуже обмеженому залозі. Давайте розберемося в реалізації транзакцій у глобалах за допомогою .
Для підтримки транзакцій в IRIS використовуються команди: , , .
1. Атомарність
Найлегше перевірити атомарність. Перевіряємо із консолі бази даних.
Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMITПотім робимо висновок:
Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)Отримаємо:
1 2 3Все в порядку. Атомарності дотримано: всі зміни записалися.
Ускладнимо завдання, введемо помилку і подивимося як збережеться транзакція, частково чи взагалі ніяк.
Ще раз перевіримо атомарність:
Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3Після чого примусово зупинимо контейнер, запустимо та подивимося.
docker kill my-irisЦя команда практично еквівалентна насильницькому вимкненню живлення, оскільки надсилає сигнал негайної зупинки процесу SIGKILL.
Можливо транзакція збереглася частково?
WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)- Ні, не збереглася.
Випробуємо команду відкату:
Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TROLLBACK
WRITE ^a(1), ^a(2), ^a(3)
^
<UNDEFINED> ^a(1)Теж нічого не збереглося.
2. Узгодженість
Оскільки в базах на глобалах ключі робляться також на глобалах (нагадаю, що глобал — це більш низькорівнева структура для зберігання даних, ніж реляційна таблиця), для виконання вимог узгодженості потрібно зміну ключа включати в ту саму транзакцію, що й зміна глобалу.
Наприклад у нас є глобал ^person, в якому ми зберігаємо персоналії і як ключ ми використовуємо ІПН.
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
...Для того, щоб мати швидкий пошук на прізвище та ім'я ми зробили ключ ^index.
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1Для того, щоб база була погоджена, ми повинні додавати персоналію так:
TSTART
^person(1234567, ‘firstname’) = ‘Sergey’
^person(1234567, ‘lastname’) = ‘Kamenev’
^person(1234567, ‘phone’) = ‘+74995555555
^index(‘Kamenev’, ‘Sergey’, 1234567) = 1
TCOMMITВідповідно, при видаленні ми також маємо використовувати транзакцію:
TSTART
Kill ^person(1234567)
ZKill ^index(‘Kamenev’, ‘Sergey’, 1234567)
TCOMMITТобто виконання вимоги узгодженості лежить повністю на плечах програміста. Але коли йдеться про глобали — це нормально, через їхню низькорівневу природу.
3. Ізольованість
Ось тут починаються нетрі. Багато користувачів одночасно працюють над однією і тією ж базою, змінюють ті самі дані.
Ситуація можна порівняти з тим, коли багато користувачів одночасно працюють з одним і тим же репозиторієм з кодом і намагаються одночасно в нього комити зміни в багатьох файлах відразу.
База даних має це все розрулювати в реальному часі. Враховуючи, що в серйозних компаніях є навіть спеціальна людина, яка відповідає за контроль версій (за злиття гілок, вирішення конфліктів тощо), а БД все це має робити в реальному часі, стає очевидною складність завдання і правильність проектування бази даних і коду, що її обслуговує.
База даних не може зрозуміти зміст дій виконуваних користувачами, щоб не допустити конфліктів, якщо вони працюють над тими самими даними. Вона може лише скасувати одну транзакцію, що суперечить іншій або виконати їх послідовно.
Ще одна проблема, що під час виконання транзакції (до комміту), стан бази може бути неузгодженим, тому бажано, щоб інші транзакції не мали доступу до неузгодженого стану бази даних, що досягається в реляційних БД багатьма способами: створення снепшотів, багатоверсійністю рядків і т.п.
При паралельному виконанні транзакцій важливо, щоб вони не заважали один одному. Це і є властивість ізольованості.
SQL визначає 4 рівні ізольованості:
- ПРОЧИТАЙТЕ НЕЗАМОВЛЕНО
- READ COMMITTED
- ПОВТОРНЕ ЧИТАННЯ
- СЕРІАЛІЗАЦІЙНИЙ
Розглянемо кожен рівень окремо. Витрати реалізації кожного рівня зростають майже експоненційно.
ПРОЧИТАЙТЕ НЕЗАМОВЛЕНО — це найнижчий рівень ізольованості, але при цьому найшвидший. Транзакції можуть читати зміни, внесені один одним.
READ COMMITTED - Це наступний рівень ізоляції, який є компромісом. Транзакції не можуть читати зміни, внесені один одним до комміту, але можуть читати будь-які зміни, внесені після комміту.
Якщо у нас є довга транзакція Т1, протягом якої пройшли комміти в транзакціях Т2, Т3... Тn, які працювали з тими самими даними, що й Т1, то при запиті даних у Т1 ми щоразу отримуватимемо різний результат. Цей феномен називається неповторне читання.
ПОВТОРНЕ ЧИТАННЯ — у цьому рівні ізоляції ми не маємо феномена неповторного читання, за рахунок того, що для кожного запиту на читання даних створюється знімок даних результату і при повторному використанні в цій же транзакції використовуються дані зі знімка. Однак у цьому рівні ізоляції можливе читання фантомних даних. Йдеться про читання нових рядків, які були додані паралельними зафіксованими транзакціями.
СЕРІАЛІЗАЦІЙНИЙ - Найвищий рівень ізоляції. Він характеризується тим, що дані, які будь-яким чином використовуються в транзакції (читання або зміна), стають доступними іншим транзакціям тільки після завершення першої транзакції.
Для початку розберемося, чи є ізоляція операцій у транзакції від основного потоку. Відкриємо 2 вікна терміналу.
Kill ^t
Write ^t(1)
2
TSTART
Set ^t(1)=2Ізоляції немає. Один потік бачить, що робить другий транзакцію, що відкрив.
Подивимося, чи бачать транзакції різних потоків те, що відбувається всередині них.
Відкриємо 2 вікна терміналу та відкриємо 2 транзакції паралельно.
kill ^t
TSTART
Write ^t(1)
3
TSTART
Set ^t(1)=3
Паралельні транзакції вбачають дані один одного. Отже, ми отримали найпростіший, але й найшвидший рівень ізоляції READ UNCOMMITED.
У принципі це можна було очікувати для глобалів, для яких швидкодія завжди ставилася в основу.
Як же бути, якщо нам знадобиться вищий рівень ізоляції в операціях на глобалах?
Тут потрібно задуматися навіщо взагалі потрібні рівні ізоляції та як вони працюють.
Найвищий рівень ізоляції SERIALIZE означає, що результат паралельно виконуваних транзакцій еквівалентний їхньому послідовному виконанню, що гарантує відсутність колізій.
Таке ми можемо зробити за допомогою грамотних блокувань в ObjectScript, які мають безліч різноманітних способів застосування: можна робити звичайне, інкрементне, множинне блокування командою .
Нижчі рівні ізоляції — це компроміси, покликані збільшити швидкість роботи бази даних.
Подивимося як ми можемо досягти різних рівнів ізоляції за допомогою блокувань.
Цей оператор дозволяє брати не тільки ексклюзивні блокування, потрібні для зміни даних, але так звані shared, які можуть брати паралельно відразу кілька потоків, коли їм потрібно читати дані, які не повинні змінюватися іншими процесами в процесі читання.
Докладніше про двофазний метод блокувань російською та англійською мовами:
→
→
Складність у тому, що під час транзакції стан бази то, можливо неузгодженим, проте ці неузгоджені дані видно іншим процесам. Як цього уникнути?
Ми зробимо за допомогою блокувань такі вікна видимості, у яких стан бази буде узгоджений. І всі звернення до таких вікон видимості узгодженого стану контролюватимуть блокування.
Shared-блокування одних і тих же даних багаторазові їх можуть взяти кілька процесів. Це блокування забороняють іншим процесам змінювати дані, тобто. вони застосовуються на формування вікон узгодженого стану БД.
Ексклюзивні блокування використовуються для змін даних – таке блокування може взяти лише один процес. Ексклюзивне блокування може взяти:
- Будь-який процес, якщо дані вільні
- Тільки той процес, який має на ці дані shared-блокування та перший запросив ексклюзивне блокування.

Чим вже вікно видимості, тим довше його доводиться чекати іншим процесам, але тим узгодженим може бути стан БД у ньому.
READ_COMMITED — суть цього рівня, що бачимо лише закоммічені дані з інших потоків. Якщо дані в іншій транзакції ще не закоммічені, ми бачимо їх стару версію.
Це дозволяє нам розпаралелити роботу замість очікування звільнення блокування.
Без спеціальних хитрощів ми не зможемо бачити стару версію даних в IRIS, тому нам доведеться обійтися блокуванням.
Відповідно, нам доведеться за допомогою shared блокувань дозволити читання даних тільки в моменти узгодженості.
Допустимо, у нас є база користувачів ^person, які переказують один одному гроші.
Момент перекладу від персони 123 до персони 242:
LOCK +^person(123), +^person(242)
Set ^person(123, amount) = ^person(123, amount) - amount
Set ^person(242, amount) = ^person(242, amount) + amount
LOCK -^person(123), -^person(242)Момент запиту кількості грошей у персони 123 перед списанням має супроводжуватися ексклюзивним блокуванням (за замовчуванням):
LOCK +^person(123)
Write ^person(123)А якщо потрібно показати стан рахунку в особистому кабінеті, то можна використовувати shared блокування або взагалі його не використовувати:
LOCK +^person(123)#”S”
Write ^person(123)Проте, якщо припустити, що операції з БД виконуються практично миттєво (нагадаю, що глобали — це набагато низькорівнева структура, ніж реляційна таблиця), необхідність цього рівня падає.
ПОВТОРНЕ ЧИТАННЯ — у цьому рівні ізоляції допускається, що може бути кілька читань даних, які можуть бути змінені паралельними транзакціями.
Відповідно нам доведеться ставити shared блокування на читання даних, які ми змінюємо та ексклюзивні блокування на дані, які ми змінюємо.
Благо, оператор LOCK дозволяє в одному операторі детально перерахувати всі необхідні блокування, яких може бути дуже багато.
LOCK +^person(123, amount)#”S”
чтение ^person(123, amount)інші операції (у цей час паралельні потоки намагаються змінити ^person(123, amount), але не можуть)
LOCK +^person(123, amount)
изменение ^person(123, amount)
LOCK -^person(123, amount)
чтение ^person(123, amount)
LOCK -^person(123, amount)#”S”При перерахуванні блокувань через кому вони беруться послідовно, а якщо зробити так:
LOCK +(^person(123),^person(242))то вони беруться атомарно все одразу.
СЕРІАЛІЗАЦІЯ — нам доведеться виставити блокування так, щоб зрештою всі транзакції, які мають загальні дані, виконувались послідовно. Для цього підходу більшість блокувань мають бути ексклюзивними та братися на найменші області глобалу для продуктивності.
Якщо ж говорити про списання коштів у глобалі ^person, то для нього прийнятний лише рівень ізоляції SERIALIZE, тому що гроші повинні витрачатися суворо послідовно, інакше можна витратити одну й ту саму суму кілька разів.
4. Довготривалість
Я проводив тести з жорстким вирубуванням контейнера за допомогою
docker kill my-irisБаза їх переносила добре. Проблем не було виявлено.
Висновок
Для глобалів у InterSystems IRIS є підтримка транзакцій. Вони справді атомарні, надійні. Для забезпечення ж узгодженості БД на глобалах необхідні зусилля програміста та використання транзакцій, оскільки в ній немає складних вбудованих конструкцій типу зовнішніх ключів.
Рівень ізоляції у глобалів без використання блокувань - це READ UNCOMMITED, а при використанні блокувань можна забезпечити його аж до рівня SERIALIZE.
Коректність і швидкість роботи транзакцій на глобалах дуже залежить від майстерності програміста: чим ширше використовуються shared-блокування при читанні, тим вищий рівень ізоляції, а чим вузько беруться ексклюзивні блокування, тим більша швидкодія.
Джерело: habr.com
