Реалізація рівнів ізоляцій у базах даних
Привіт! Мене звати Віталій Брецко, я Senior Developer в компанії GlobalLogic. Зараз працюю над одним із проєктів Hitachi. У цій статті я хочу розкрити тему механізмів, які використовуються для ізоляції рівнів баз даних. Це питання є досить комплексним. Тому, на мою думку, зазирнути глибше і зрозуміти не лише базові класифікації, а й специфічні принципи роботи, є корисно і цікаво водночас.
Спочатку варто звернути увагу на короткий огляд теорії, щоб зрозуміти, про що йтиме мова далі.
Короткий огляд базової теорії, щоб освіжити знання:
CAP — теорема, яка стверджує, що при будь-якій реалізації бази даних неможливо одночасно забезпечити такі властивості: availability, consistency, partition tolerance.
Транзакція — набір SQL-команд, які об’єднані в один фізичний і логічний блок і вважаються, згідно з ACID, найменшим неподільним об’єктом.
ACID — властивості транзакції :
- Atomicity — транзакція є атомарною; це означає, що вона виконується повністю або взагалі не виконується, при цьому зміни повертаються до стану, який був на початку запуску транзакції.
- Consistency — операції, які виконує транзакція, не повинні переводити стан даних в неузгоджений стан (порушення FK, PK, Unique key та інших обмежень даних).
- Isolation — транзакції під час паралельного виконання не повинні впливати одна на одну.
- Durability — дані не мають часового обмеження, ми вважаємо їх вічними; при екстреній ситуації потрібно використовувати відновлювальні механізми, які можуть відтворити стан даних та операцій, що виконувались під час цієї ситуації.
Види аномалій
Бази даних працюють під великим навантаженням, тому мають значні обчислювальні ресурси (CPU і RAM), які використовуються також і для виконання транзакцій паралельно. Транзакції працюють з даними, і під час цього виникають різні аномалії, що залежать від таких характеристик: паралелізм, актуальність даних, ізольованість. Для того щоб змінювати ці три характеристики, існують рівні ізоляції. При найменшому рівні ізоляції ми отримуємо найбільший перформанс і навпаки. Насамперед варто розглянути усі можливі аномалії для того, щоб краще зрозуміти, як кожна з баз даних вирішує ці проблеми.
Lost update
Найбільш примітивний вид аномалії, при якому відбувається одночасна зміна блоку даних транзакціями і одна зі змін втрачається. Наприклад, T1 i T2 працюють над записом з полем foo = 10, T1 додає до поля 5 а T2 додає 15. У результаті ми очікуємо, що буде 30, але транзакція T1 не знає нічого про T2 і навпаки. Вони працюють із полем foo, яке знаходиться в їхній пам’яті, а не з реальним значенням, що міститься в базі, тому зміни T1 або T2 будуть втрачені.
Dirty read
Аномалія, при якій транзакції можуть читати не закомічені дані. Наприклад, T1 i T2 працюють над записом із полем foo =10, T1 додає 5, а T2 додає 15. У цьому випадку, якщо одна транзакція змінила поле, інша побачить це та буде працювати з новим полем. Але якщо одна з транзакцій не виконається після того, як інша завершить своє виконання, дані не будуть повернуті в попередній стан. Тобто якщо T1 завершить свою роботу раніше Т2, в полі foo буде 30, а не 25.
Non-Repeatable read
Аномалія, при якій транзакція читає один і той самий запис в рамках однієї транзакції, але отримує різні значення. Наприклад, T1 читає запис, через деякий час T2 закінчує оновлення полів цього запису і при повторному читанні T1 отримує інший результат значень полів.
Phantom read
Аномалія, що схожа до Non-repeatable read, але при виконанні операцій, де результатом є певна множина, вона може містити більше/менше записів відносно попередньої операції.
Я розглянув найпоширеніші аномалії, проте існують більш екзотичні, наприклад:
- Serialization anomaly — група транзакцій, що успішно виконані, при будь-якому порядку не можуть забезпечити консистентність.
- Write skews — транзакції прочитують спільні дані та одночасно їх змінюють та записують; це є проблемою, коли існують логічні умови для модифікації даних.
- Halloween — під час операції апдейту запис може зміститись вверх та бути обробленим повторно. До прикладу, оновлення зарплатні робітників на 20% , якщо вони заробляють < 2000.
Усі ці аномалії виникають із різною частотою. Деякі з них досить легко вирішити, проте такі випадки, як Serialization anomaly та Halloween виправити відносно важко і дороговартісно.
Механізми вирішення
Загальний протокол SQL гарантує наявність чотирьох рівнів ізоляції з різною кількістю обмежень. Вони розподілені таким чином:
Проте на практиці ця картина може відрізнятися залежно від конкретної бази даних. Тож розглянемо реалізацію кожного рівня ізоляції на прикладах різних СУБД.
PostgreSQL
Досить поширена база даних завдяки open-source-ності і великій кількості типів даних. Вона підтримує всі чотири рівні ізоляції бази даних. У випадку postgreSQL Read uncommitted є тим самим, що й Read committed, тому дефолтний рівень ізоляції є Read committed. Це зумовлене використанням MVCC.
MVCC — модель управління за допомогою версіонування даних. Конкурентний доступ транзакцій до зміни рядка в реальності не змінює наявний рядок, а створює нову копію рядка з новою версією, а попередню залишає до моменту приходу очисника, так званого Vacuum. Ідея полягає в зменшенні використання блокувань, оскільки транзакція може тривати довгий час і прочитані дані й дані, що опрацьовуються, будуть заблоковані також.
Але в такого підходу є недолік, а саме — зменшується швидкість читання через зберігання різних версій одного рядка, і в цьому випадку індекси працюють не так ефективно. Також варто пам’ятати, що кожна транзакція має доступ до старих версій рядка, якщо вони ще не очищені і мають системні поля, що містять xmin/xmax, які вказують на id транзакції, що створила поле або помітила його як видалене.
Перейдемо до огляду реалізації ізольованості, почнемо із Read committed, оскільки це дефолтний рівень.
Read committed
Операція читання. У загальному використанні без FOR UPDATE/SHARE операції працюють зі snapshot-даними, ніякі блокування полів для читання (shared lock) не застосовуються. Snapshot будується по певному алгоритму, що використовує системні поля запису xmin/xmax для фільтрування закоміченості даних. Важливо пам’ятати, що snapshot дійсний тільки для однієї select-операції, і він перебудовується під час кожного select-у, через що виконання одного запиту може отримати різні результати (Non-repeatable read anomaly). Також для оптимізації створення snapshot-а використовується механізм Visibility map.
Схематичний алгоритм створення snapshot:
Операція оновлення. Використовує exclusive row lock, оскільки одночасно n транзакцій можуть оновлювати запис. До кожного оператора зміни додається версіонування, тобто WHERE version = :version, і, якщо все пройшло успішно і транзакція закомітила зміни, йде створення нового запису з оновленими xmin/xmax, що використовуються для будування snapshot. Оновлення також працює зі snapshot-ом даних під час пошуку. Якщо на шуканому полі встановлено exclusive lock з іншої транзакції, заблокована транзакція очікує зняття lock-у. У цьому випадку можливі три сценарії:
- Блокуюча транзакція відкотила зміни -> заблокована транзакція проводить свої зміни.
- Блокуюча транзакція закомітила зміни -> заблокована транзакція спробує провести зміни, але застосує їх до вже нової версії запису.
- Блокуюча транзакція видалила запис -> заблокована транзакція проігнорує це поле.
Repeatable Read
Операція читання. Транзакція весь час буде працювати тільки з однією копією snapshot-а, але власні зміни будуть видимі для всіх операцій всередині транзакції. Важливо пам’ятати, що зазвичай snapshot формується під час першої non-transaction-control команди, тому в реальному snapshot-і можливий незначний зсув.
Варто зауважити, що використання цього рівня ізоляції може мати логічне порушення бізнес-правил через неконсистентність snapshot-а. Наприклад, ви проводите batch-оновлення та параллельно запустили read-транзакцію, яка може збудувати snapshot на середині batch-оновлення. Для читання цей знімок буде повністю валідний, але він не зможе побачити всі деталі batch-оновлення. Для уникнення цієї ситуації потрібно використовувати Explicit lock для блокування транзакцій.
Операція оновлення. Транзакція на цьому рівні ізоляції не може використовувати exclusive lock для полів, що були модифіковані іншою транзакцією, або змінювати модифіковані поля. Якщо ж транзакція спробує це зробити, вона отримає помилку серіалізації. Усі зміни повинні відкотитись, і потрібно спробувати провести транзакцію знову.
Механізм, що гарантує уникнення Phantom read. Під час змін нових\паралельних транзакцій за допомогою фільтрування за xmin встановлюється значення xmin більше за наявну транзакцію, тому snapshot не буде містити оновлені версії рядків.
***Помилка серіалізації присутня тільки в non-read транзакціях, і тільки ці транзакції потрібно спробувати провести знову.
Serializable Snapshot Isolation
Цей рівень слід використовувати лише у разі, якщо потрібно усунути серіалізаційну аномалію (write-read skew).
Для цього PostgreSQL будує графи та використовує логічні блокування (вони не блокують ані читання, ані запис, а лише впливають на побудову графа). Транзакція досі працює лише зі своїм snapshot-ом, який створюється під час виконання першої non-transaction-control команди, але також сам PostgreSQL збирає інформацію про транзакції: що вони роблять (читають/пишуть), які діапазони даних захоплюють та де перетинаються. Використовуючи при цьому Serial index read locks і Predicate lock.
Predicate lock — якщо транзакції взаємодіють із перетинними множинами, встановлюється цей лок для відстеження конфліктів під час вставки/оновлення.
Формування графа:
- Спочатку встановлюються вершини (транзакції) графа.
- Потім встановлюються напрямлені ребра між взаємодіючими вершинами (транзакціями) та визначається вид залежності (read/write, write/write).
- Якщо не виникла циклічна залежність, серіалізація транзакцій пройде успішно, інакше одна з транзакцій відкочується.
Короткий висновок
MVCC (Multiversion Concurrency Control) дозволяє зменшити кількість блокувань під час транзакцій за рахунок створення нових версій рядків замість їх безпосередньої зміни. Це забезпечує конкурентний доступ до даних, проте може знизити швидкість читання через накопичення старих версій рядків.
Для забезпечення консистентності даних під час роботи з високими рівнями ізоляції важливо враховувати можливість серіалізаційних помилок і правильно налаштовувати блокування транзакцій.
MySQL InnoDB
Аналогічно до PostgreSQL підтримує чотири рівні ізоляції, але використовує специфічні механізми, як gap lock i next-key lock.
Read committed
Операція читання. При кожному читанні формується read view, щось схоже до snapshot як в postgreSQL.
Операція оновлення. Використовується exclusive row lock, що утримується до кінця транзакції, також якщо інша транзакція оновила поле, відбувається re-check read view, щоб мати актуальні дані.
Repeatable read
Операція читання. На цьому рівні ізоляції під час виконання першого запиту створюється read view, і далі транзакція працює лише з ним. Щоб уникнути ситуації, коли два запити SELECT з однаковою умовою можуть повернути різні результати через додавання нового рядка іншою транзакцією, використовується механізм gap lock. Gap lock — це механізм, який блокує певний проміжок в індексі, запобігаючи вставці нових рядків у цей проміжок до завершення транзакції.
Операція оновлення. Транзакція завжди працює з read view, створеним на початку транзакції, але додатково використовує механізм next key locks на знайдені рядки та діапазони. Next key locks — це механізм, що не лише забороняє іншим транзакціям змінювати заблоковані поля, але й не дозволяє вставляти або видаляти записи в межах діапазону блокування. Завдяки цьому механізму забезпечується захист від фантомного читання.
Serialization
На відміну від PostgreSQL, тут не використовується Serializable Snapshot Isolation (SSI). Замість цього застосовується підхід, за якого читаючі транзакції стають «жорстко блокувальними». Для цього використовується комбінація shared lock та gap lock, що дозволяє максимально уникнути проблем із серіалізацією.
Якщо ж виникає ситуація, коли одна транзакція читає дані, а інша намагається вставити або оновити запис, який підпадає під діапазон читання, виникає конфлікт.
Такий конфлікт може бути вирішений двома способами:
- Deadlock — коли транзакції взаємно блокують одна одну, і система автоматично відкотить одну з транзакцій.
- Блокування та очікування — транзакція, що бажає змінити дані, очікує завершення читаючої транзакції, зберігаючи консистентність даних.
Короткий висновок
MySQL із механізмом InnoDB має багато схожих механізмів із PostgreSQL, проте існують і важливі відмінності. Зокрема, InnoDB використовує більше механізмів блокування, що може негативно впливати на продуктивність порівняно з PostgreSQL. Однак, варто зазначити, що MySQL спочатку розроблялась як база даних, орієнтована більше на читання даних, аніж на їх запис
Oracle
В цій базі даних наявні тільки два рівні ізоляції.
Read committed
Операція читання. Як і в двох попередніх підходах, під час читання створюється знімок даних (snapshot). Проте особливістю цього знімка в Oracle є його розміщення у файловій структурі. На відміну від інших систем, Oracle не зберігає старі версії даних поруч з актуальними, а використовує для цього undo-сегменти.
Undo-сегменти дозволяють відновлювати попередні версії рядків у випадку відкату транзакції або під час запитів, що потребують історичних даних. Це підвищує ефективність роботи з великими обсягами інформації та знижує навантаження на основні таблиці.
Операція оновлення. Основна відмінність відчутна, коли застосовується exclusive row lock (ексклюзивне блокування рядка). У Oracle це блокування встановлюється не під час виконання операцій оновлення, а лише після виконання команди COMMIT або ROLLBACK.
Oracle Serializable
Oracle використовує один загальний знімок для транзакції. Якщо ж дані оновились, то транзакція може побачити старі версії за допомогою undo-лога і наявністю SCN (System Change Number — точка в часі). Блокування ж відбувається тільки під час завершення транзакції, більшість часу читачі читають. Якщо виникає конфлікт під час оновлення даних, тоді буде викинута помилка сериалізації.
Read only
Спеціальний режим роботи, у якому заборонені будь-які команди data management (керування даними). Дозволено лише одне читання, яке виконується зі сформованого знімка даних, створеного на початку читання. Цей режим зазвичай використовують під час створення звітів або рапортів, оскільки не застосовуються блокування, а відбувається лише читання зі знімка.
Загальний висновок
Бази даних використовують різні механізми для забезпечення різних рівнів ізоляції транзакцій. Залежно від обраного механізму можуть виникати проблеми під час роботи з індексами та іншими структурами даних. Важливо розуміти особливості кожного рівня ізоляції та враховувати їхні можливі наслідки для вашої системи.
7 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів