Уяви, що ти граєш в онлайн-гру, а в цей час якийсь читер лізе в її код і прокачує свого персонажа, або читаєш книжку в бібліотеці, а хтось у цей момент може міняти сторінки, вставляти нові глави чи взагалі підміняти книжку. Досить неприємна ситуація, правда? Саме від таких "сюрпризів" і захищає рівень ізоляції REPEATABLE READ.
REPEATABLE READ дає гарантію, що дані, які ти бачиш всередині однієї транзакції, залишаться незмінними до кінця цієї транзакції. Навіть якщо інша транзакція спробує оновити ці дані, твоя транзакція буде застрахована від таких змін.
Ключові фішки:
- Запобігає
Dirty Read(читання даних, які ще не підтверджені). - І найголовніше — запобігає
Non-Repeatable Read. Це означає, що якщо ти прочитав набір даних на початку транзакції, то при повторному читанні отримаєш ті ж самі дані, навіть якщо інший користувач їх змінив.
Але REPEATABLE READ не захищає від Phantom Read. Якщо інша транзакція додасть нові рядки, вони можуть з'явитися у твоєму повторному запиті. Щоб прибрати і цю аномалію, потрібен рівень SERIALIZABLE, але про це поговоримо пізніше.
Як встановити рівень ізоляції REPEATABLE READ
Перш ніж перейти до прикладів, давай розберемося, як увімкнути цей рівень ізоляції у PostgreSQL. Є два основних способи:
Встановити рівень ізоляції для конкретної транзакції:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; -- Твої запити COMMIT;Встановити рівень ізоляції для поточної сесії:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
У другому випадку всі транзакції в поточній сесії будуть використовувати REPEATABLE READ.
Приклад: запобігання Non-Repeatable Read
Припустимо, у нас є таблиця accounts з такою структурою:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
customer_name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
);
INSERT INTO orders (customer_name, status)
VALUES ('Аліса', 'pending'), ('Боб', 'pending');
Почнемо з базового сценарію, коли одна транзакція змінює дані, а інша читає.
Сценарій без REPEATABLE READ (рівень READ COMMITTED)
Транзакція 1 починає роботу:
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Отримуємо: 100
У цей час Транзакція 2 змінює дані:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
Транзакція 1 продовжує:
SELECT balance FROM accounts WHERE account_id = 1;
-- Отримуємо: 150 (дані змінилися!)
COMMIT;
Як бачиш, при рівні READ COMMITTED дані можуть змінитися між двома читаннями в межах однієї транзакції. Це і є Non-Repeatable Read.
Сценарій з REPEATABLE READ
Тепер спробуємо той самий приклад, але з рівнем ізоляції REPEATABLE READ.
Транзакція 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT balance FROM accounts WHERE account_id = 1;
-- Отримуємо: 100
Транзакція 2:
BEGIN;
UPDATE accounts SET balance = 150 WHERE account_id = 1;
COMMIT;
Транзакція 1 продовжує роботу:
SELECT balance FROM accounts WHERE account_id = 1;
-- Все ще отримуємо: 100 (дані незмінні!)
COMMIT;
Незалежно від змін, зроблених іншою транзакцією, транзакція 1 бачить дані такими, якими вони були на момент старту. Таким чином, Non-Repeatable Read запобігається.
Як працює REPEATABLE READ
PostgreSQL використовує механізм MVCC (Multi-Version Concurrency Control), щоб реалізувати рівень ізоляції REPEATABLE READ. Основний принцип MVCC — кожна транзакція отримує стабільний "знімок" бази, який не змінюється до завершення транзакції. Це досягається за рахунок створення і керування кількома версіями рядків.
Коли транзакція стартує, вона бачить дані в тому стані, в якому вони були на момент її початку. Якщо інша транзакція вносить зміни, PostgreSQL створює нову версію рядка, але попередня версія залишається для всіх транзакцій, які її використовують.
Саме тому транзакції працюють повільніше і потребують більше пам'яті. І саме тому мало хто сидить на найсильнішому рівні ізоляції: він найнадійніший, але найбільше гальмує роботу з базою.
Обмеження REPEATABLE READ: Phantom Read
Як ми вже згадували, REPEATABLE READ не захищає від Phantom Read. Щоб зрозуміти, що це таке, розглянемо приклад із запитами, які працюють з діапазонами даних.
Припустимо, у нас є таблиця orders:
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL
);
INSERT INTO orders (amount)
VALUES (50), (100), (150);
Транзакція 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Отримуємо: 2
Транзакція 2:
BEGIN;
INSERT INTO orders (amount) VALUES (200);
COMMIT;
Транзакція 1 продовжує роботу:
SELECT COUNT(*) FROM orders WHERE amount > 50;
-- Отримуємо: 3 (новий рядок з'явився в результаті запиту!)
COMMIT;
У цьому випадку новий рядок (з amount = 200) був доданий іншою транзакцією, і він "фантомно" з'явився у результаті запиту транзакції 1, незважаючи на рівень ізоляції REPEATABLE READ.
Якщо тобі потрібно уникнути Phantom Read, доведеться використовувати рівень SERIALIZABLE, але це завжди компроміс із продуктивністю.
Плюси і мінуси REPEATABLE READ
Рівень ізоляції REPEATABLE READ — топовий варіант, коли тобі треба бути впевненим, що дані не зміняться під час виконання транзакції. Як тільки ти щось прочитав — це значення залишиться таким до самого COMMIT, навіть якщо хтось в іншій транзакції спробує внести зміни.
Такий підхід запобігає і брудним читанням (dirty read), і неповторюваному читанню (non-repeatable read). Ти працюєш із тими ж самими даними, що й на старті — ніяких неочікуваних апдейтів "на льоту". Це особливо корисно, коли ти формуєш звіти або приймаєш рішення, де важлива консистентність.
З іншого боку, REPEATABLE READ не справляється з так званими "фантомами" (phantom read) — коли нові рядки з'являються у результаті запиту, який ти вже робив у межах тієї ж транзакції. Крім того, при великому навантаженні такий рівень може викликати конфлікти між транзакціями, особливо якщо вони часто звертаються до одних і тих же даних. Це може призвести до блокувань і відкатів, навіть якщо із запитами все було ок.
В цілому, REPEATABLE READ — це класний баланс надійності та продуктивності, але в сценаріях із високою конкуренцією може вимагати додаткових налаштувань і уваги.
Корисні поради і типові помилки
- Пам'ятай, що вибір рівня ізоляції впливає на продуктивність. Використовуй
REPEATABLE READтільки тоді, коли потрібна впевненість у незмінності даних. - Плутанина між
REPEATABLE READіSERIALIZABLE— часта помилка. Якщо ти бачиш нові рядки у результаті повторного запиту — це очікувана поведінка дляREPEATABLE READ. - Працюючи з довгими транзакціями, будь обережний із можливими конфліктами блокувань. Довгі транзакції можуть заблокувати інші операції.
PostgreSQL дає різні інструменти для керування ізоляцією транзакцій. Рівень REPEATABLE READ ідеально підходить для випадків, коли важлива впевненість у незмінності вже прочитаних даних у межах однієї транзакції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ