Реалізація рівнів ізоляцій у базах даних

Привіт! Мене звати Віталій Брецко, я Senior Developer в компанії GlobalLogic. Зараз працюю над одним із проєктів Hitachi. У цій статті я хочу розкрити тему механізмів, які використовуються для ізоляції рівнів баз даних. Це питання є досить комплексним. Тому, на мою думку, зазирнути глибше і зрозуміти не лише базові класифікації, а й специфічні принципи роботи, є корисно і цікаво водночас.

Спочатку варто звернути увагу на короткий огляд теорії, щоб зрозуміти, про що йтиме мова далі.

Короткий огляд базової теорії, щоб освіжити знання:

CAP — теорема, яка стверджує, що при будь-якій реалізації бази даних неможливо одночасно забезпечити такі властивості: availability, consistency, partition tolerance.

Транзакція — набір SQL-команд, які об’єднані в один фізичний і логічний блок і вважаються, згідно з ACID, найменшим неподільним об’єктом.

ACID — властивості транзакції :

  1. Atomicity — транзакція є атомарною; це означає, що вона виконується повністю або взагалі не виконується, при цьому зміни повертаються до стану, який був на початку запуску транзакції.
  2. Consistency — операції, які виконує транзакція, не повинні переводити стан даних в неузгоджений стан (порушення FK, PK, Unique key та інших обмежень даних).
  3. Isolation — транзакції під час паралельного виконання не повинні впливати одна на одну.
  4. 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, але при виконанні операцій, де результатом є певна множина, вона може містити більше/менше записів відносно попередньої операції.

Я розглянув найпоширеніші аномалії, проте існують більш екзотичні, наприклад:

  1. Serialization anomaly — група транзакцій, що успішно виконані, при будь-якому порядку не можуть забезпечити консистентність.
  2. Write skews — транзакції прочитують спільні дані та одночасно їх змінюють та записують; це є проблемою, коли існують логічні умови для модифікації даних.
  3. 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 для блокування транзакцій.

16-17 травня, Київ. Квитки тут!👇

Операція оновлення. Транзакція на цьому рівні ізоляції не може використовувати 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 — якщо транзакції взаємодіють із перетинними множинами, встановлюється цей лок для відстеження конфліктів під час вставки/оновлення.

Формування графа:

  1. Спочатку встановлюються вершини (транзакції) графа.
  2. Потім встановлюються напрямлені ребра між взаємодіючими вершинами (транзакціями) та визначається вид залежності (read/write, write/write).
  3. Якщо не виникла циклічна залежність, серіалізація транзакцій пройде успішно, інакше одна з транзакцій відкочується.

Короткий висновок

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 (керування даними). Дозволено лише одне читання, яке виконується зі сформованого знімка даних, створеного на початку читання. Цей режим зазвичай використовують під час створення звітів або рапортів, оскільки не застосовуються блокування, а відбувається лише читання зі знімка.

Загальний висновок

Бази даних використовують різні механізми для забезпечення різних рівнів ізоляції транзакцій. Залежно від обраного механізму можуть виникати проблеми під час роботи з індексами та іншими структурами даних. Важливо розуміти особливості кожного рівня ізоляції та враховувати їхні можливі наслідки для вашої системи.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось20
До обраногоВ обраному15
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

1. Осталось привести примеры зачем менять уровень изоляции и когда, то что это все и так разжевано в википедии.
2. Проблемы репликации, когда часть r/o запросов выносят на реплику, а вы на мастере понизили уровень изоляции, и реплика немного отстает от мастера

ізоляції рівнів баз даних

Може, «рівнів ізоляції»?

Було б варто зауважити, що ANSI SQL SERIALIZABLE та SNAPSHOT ISOLATION — це різні речі, і що SERIALIZABLE в Postgres — це реально SNAPSHOT ISOLATION (так, в статті згадується SSI, але так, наче це те ж саме, що і SQL SERIALIZABLE)

цікаво, я завжди вважав, що вже Repeatable read в Postgres — це фактично SNAPSHOT, може якраз через відсутність вже на цьому рівні phantom read

CAP згадано в статті, але більше ніяк не використано. Заготовка для наступної статті? З цеї краще видалити, щоб не бентежило.
Також було б гарно додати посилання на основи — і «банальну» вікіпедію, і розширену базу типу такої. Взагалі без посилань і без слов для подальшого пошуку виглядає якось дивно.

і розширену базу типу такої.

Та стаття звісно важлива, але я б не називав її «розширеною базою», там описані більше логічні помилки (ТС про це написав), які має вирішувати програміст, а не гарантувати СУБД.

Щодо КАП ви праві. Хотілося б дізнатись чи автор комусь давав статтю на вичитку, вона виглядає як занадто спрощена компіляція

Гарна стаття.

В тeкстi явно нe сказано що в Mysql при Repeatable read можливi lost updates навiдмiну вiд постгрeсу.

Однак, варто зазначити, що MySQL спочатку розроблялась як база даних, орієнтована більше на читання даних, аніж на їх запис

То вжe дискусiйнe питання як воно спочатку було, локiв можe бiльшe, алe нiколи нe було дублювання рядкiв при апдeйтах, розбухання таблиць та вакууму. То зараз в PG стало кращe з цим.

Підписатись на коментарі