text
PostgreSQL викидає помилку серіалізації, коли транзакція (з рівнем ізоляції Repeatable Read або Serializable) намагається оновити рядок, який вже був оновлений іншою транзакцією, щоб запобігти аномаліям, таким як втрачені оновлення і читання з перекручуванням. У цій статті я поясню, як PostgreSQL виявляє конфлікти записів серед кількох транзакцій.
Втрачене оновлення
Створимо таблицю інвентарю з двома записами. Спершу подивимося, як транзакція поводиться на різних рівнях ізоляції.
postgres=# CREATE TABLE inventory(id int, name varchar(50), qty int);
CREATE TABLE
postgres=# INSERT INTO inventory(id, name, qty) values(1, 'active', 100);
INSERT 0 1
postgres=# INSERT INTO inventory(id, name, qty) values(2, 'reserve', 200);
INSERT 0 1
На рівні ізоляції Read Committed, коли транзакція виявляє, що інша транзакція вже оновила рядок, вона переписує нову версію цього рядка і виконує оновлення цього рядка. Read Committed дозволяє аномалії, такі як втрачені оновлення, неперевірені читання та фантомні читання.
На рівні ізоляції Serializable або Repeatable Read, коли транзакція виявляє, що інша транзакція вже оновила рядок, вона буде скасована, і викидається помилка “не вдалося серіалізувати через одночасне оновлення”. Serializable позбавлене всіх аномалій. Ось як PostgreSQL визначає, чи був рядок оновлений іншою транзакцією.
PostgreSQL ніколи не виконує оновлення рядка на місці, якщо ви не створите таблицю, використовуючи інший механізм зберігання, такий як Zheap. Як результат, кілька версій одного рядка можуть існувати на сторінці. Однією з переваг цього підходу є те, що транзакція, яка створена для оновлення рядка, може визначити, чи був рядок уже оновлений іншою транзакцією. Давайте розглянемо, як це відбувається на практиці.
Типовий рядок складається з двох частин: заголовка та фактичних даних. Заголовок містить метадані про рядок. У цій статті ми зосередимося на двох атрибутах заголовка, які є вирішальними для розуміння решти вмісту: xmin та xmax. Обидва ці атрибути представляють ідентифікатори транзакцій. Коли рядок створюється, його значення xmin
встановлюється як ідентифікатор транзакції команди INSERT
. Коли рядок оновлюється, його значення xmax
встановлюється як ідентифікатор транзакції команди UPDATE
, і значення xmin
нової версії рядка буде рівним значенню xmax
попередньої версії. Є ще два інші біти підказок, що є частиною заголовка рядка і вказують, чи транзакція була підтверджена або скасована. Коли транзакція (назвемо її T1) намагається змінити рядок, вона спочатку перевіряє, чи значення xmax встановлено в ідентифікатор іншої транзакції (назвемо її T0).
Якщо значення xmax встановлено в T0, T1 перевірить статус T0 в журналі підтверджень:
- Якщо T0 підтверджено, T1 викине помилку серіалізації.
- Якщо T0 все ще в процесі, T1 чекатиме завершення T0. Залежно від результату T0, T1 або підтвердить транзакцію, або скасує її та викине помилку серіалізації.
Рівень ізоляції Serializable запобігає всім стандартним та нестандартним аномаліям, забезпечуючи сувору консистентність даних. Однак він погано масштабується в умовах високої конкуренції. Коли кілька транзакцій змагаються за зміну рядка, лише одна може бути підтверджена, що може знизити продуктивність і масштабованість під високим навантаженням.
Перекладено з: A Deep Dive into the ‘Could Not Serialize Due to Concurrent Update’ Error