Пошук уроків, статей та іншого контенту
Навчитеся використовувати Repeatable Read для стабільного знімка даних протягом транзакції.
Repeatable Read — це рівень ізоляції транзакцій, за якого всі звичайні запити в межах однієї транзакції бачать стабільний знімок даних.
У PostgreSQL цей знімок створюється під час виконання першого запиту, який не є командою керування транзакцією. Усі наступні запити використовують той самий знімок, навіть якщо паралельні транзакції вже зафіксували зміни.
За замовчуванням PostgreSQL використовує рівень Read Committed, тому кожен окремий запит може бачити новий стан бази. Repeatable Read змінює цю поведінку.
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM products;
-- Навіть якщо інша транзакція додасть product,
-- цей запит побачить той самий знімок даних.
SELECT count(*) FROM products;
COMMIT;Рівень можна встановити безпосередньо під час запуску транзакції:
BEGIN ISOLATION LEVEL REPEATABLE READ;Або за допомогою START TRANSACTION:
START TRANSACTION ISOLATION LEVEL REPEATABLE READ;Також рівень можна встановити для поточної транзакції після BEGIN, але до першого звичайного запиту:
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM products;
COMMIT;Команда SET TRANSACTION повинна виконуватися до запиту, який читає або змінює дані. Після першого такого запиту змінити рівень ізоляції для поточної транзакції вже не можна.
За потреби рівень можна встановити для всіх нових транзакцій поточного з'єднання:
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;Після цього нові транзакції в цьому з'єднанні використовуватимуть Repeatable Read, якщо інший рівень не вказано явно.
Розглянемо приклад із двома з'єднаннями до PostgreSQL.
Підготуємо таблицю:
DROP TABLE IF EXISTS products;
CREATE TABLE products (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
price numeric(10, 2) NOT NULL
);
INSERT INTO products (name, price)
VALUES
('Keyboard', 80.00),
('Mouse', 30.00);У першому з'єднанні запускаємо транзакцію та виконуємо перший запит:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) AS product_count
FROM products;Результат:
product_count
---------------
2У цей момент транзакція отримала знімок даних.
В іншому з'єднанні додаємо товар і фіксуємо зміни:
BEGIN;
INSERT INTO products (name, price)
VALUES ('Monitor', 250.00);
COMMIT;Зміна вже зафіксована й доступна для нових транзакцій.
Виконуємо той самий запит у першій транзакції:
SELECT count(*) AS product_count
FROM products;Результат усе ще буде таким:
product_count
---------------
2Товар Monitor не видно, тому що транзакція продовжує використовувати знімок, створений під час першого запиту.
Завершуємо транзакцію:
COMMIT;Після цього новий запит уже виконається в новій транзакції та побачить актуальні дані:
SELECT count(*) AS product_count
FROM products;Результат:
product_count
---------------
3Транзакція бачить власні зміни одразу після їх виконання. При цьому вона не бачить зміни, зафіксовані іншими транзакціями після створення її знімка.
BEGIN ISOLATION LEVEL REPEATABLE READ;
INSERT INTO products (name, price)
VALUES ('Webcam', 100.00);
SELECT name
FROM products
WHERE name = 'Webcam';
COMMIT;Запит побачить Webcam, оскільки цей рядок створила поточна транзакція.
Водночас рядки, додані паралельними транзакціями після створення знімка, залишаються невидимими до завершення поточної транзакції.
Одне з типових застосувань Repeatable Read — кілька разів прочитати один і той самий рядок і бути впевненим, що його значення не зміниться протягом транзакції.
Нехай одна транзакція читає товар:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT id, name, price
FROM products
WHERE id = 1;Після цього інша транзакція змінює ціну та фіксує зміни:
UPDATE products
SET price = 95.00
WHERE id = 1;
COMMIT;Перша транзакція знову читає товар:
SELECT id, name, price
FROM products
WHERE id = 1;Вона побачить ту саму ціну, що й під час першого запиту. Нове значення стане видимим після завершення першої транзакції та початку нової.
Repeatable Read не означає, що будь-яку зміну можна безпечно перезаписати. Якщо інша транзакція змінила рядок після створення знімка, PostgreSQL може завершити операцію помилкою серіалізації.
Приклад:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT id, price
FROM products
WHERE id = 1;BEGIN;
UPDATE products
SET price = 95.00
WHERE id = 1;
COMMIT;UPDATE products
SET price = 90.00
WHERE id = 1;PostgreSQL може повернути помилку на кшталт:
ERROR: could not serialize access due to concurrent updateЦе захищає транзакцію від оновлення рядка на основі застарілого знімка. У такому випадку застосунок повинен:
відкликати або відкотити поточну транзакцію;
за потреби повторити всю транзакцію;
повторно прочитати актуальні дані;
виконати операцію ще раз за новими умовами.
Після помилки транзакцію потрібно завершити:
ROLLBACK;Повторювати потрібно всю логічну транзакцію, а не лише окремий запит.
Приклад на JavaScript із бібліотекою pg:
import pg from "pg";
const { Pool } = pg;
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
async function updateProductPrice(productId, newPrice) {
const client = await pool.connect();
try {
await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ");
const result = await client.query(
`
SELECT price
FROM products
WHERE id = $1
`,
[productId],
);
if (result.rowCount === 0) {
throw new Error("Товар не знайдено");
}
await client.query(
`
UPDATE products
SET price = $1
WHERE id = $2
`,
[newPrice, productId],
);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK");
// Код 40001 означає помилку серіалізації.
if (error.code === "40001") {
throw new Error("Транзакцію потрібно повторити");
}
throw error;
} finally {
client.release();
}
}У реальному застосунку повтор транзакції зазвичай реалізують обмежену кількість разів із невеликою паузою між спробами. Важливо, щоб кожна повторна спроба створювала нову транзакцію.
У цьому режимі кожен запит отримує власний знімок:
BEGIN;
SELECT count(*) FROM products;
-- Паралельна транзакція додає рядок і виконує COMMIT.
SELECT count(*) FROM products;
-- Другий запит може побачити вже оновлену кількість.
COMMIT;Тому два однакові запити в одній транзакції можуть повернути різні результати.
У цьому режимі всі запити використовують один знімок:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT count(*) FROM products;
-- Паралельна транзакція додає рядок і виконує COMMIT.
SELECT count(*) FROM products;
-- Результат залишається таким самим, як у першого запиту.
COMMIT;Вибір рівня залежить від вимог операції:
Read Committed підходить для більшості коротких незалежних запитів;
Repeatable Read корисний, коли кілька запитів повинні працювати з одним узгодженим станом даних.
Цей рівень доречний, коли транзакція:
виконує кілька пов'язаних читань;
формує звіт або підсумок на основі узгодженого стану;
перевіряє умови перед наступною операцією;
повинна повторно прочитати дані без зміни результату;
працює з набором рядків, який не повинен змінюватися для цієї транзакції.
Наприклад, транзакція може прочитати кілька показників, перевірити їх і записати результат, гарантуючи, що всі прочитані значення належать одному знімку.
Поки транзакція Repeatable Read залишається відкритою, PostgreSQL повинен зберігати можливість бачити версії рядків, доступні її знімку.
Тому довгі транзакції можуть:
утримувати старі версії рядків;
перешкоджати очищенню непотрібних версій;
збільшувати навантаження на обслуговування таблиць;
довше утримувати блокування, якщо транзакція також змінює дані.
Практичні правила:
не залишати транзакцію відкритою під час очікування введення користувача;
виконувати всі потрібні запити якомога швидше;
завжди завершувати транзакцію через COMMIT або ROLLBACK;
у застосунках встановлювати тайм-аути для транзакцій.
BEGIN одразу створює знімокУ PostgreSQL знімок для Repeatable Read створюється під час першого звичайного запиту, а не обов'язково в момент виконання BEGIN.
Важливо, що до першого читання не слід виконувати операції, які мають бути відносно початкового стану транзакції.
SET TRANSACTION після першого запитуНеправильно:
BEGIN;
SELECT now();
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;Рівень потрібно встановити до першого запиту:
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT now();Або вказати його одразу в BEGIN:
BEGIN ISOLATION LEVEL REPEATABLE READ;У Repeatable Read нові коміти інших транзакцій не з'являються в поточному знімку. Щоб побачити їх, потрібно завершити поточну транзакцію та розпочати нову.
Помилка could not serialize access due to concurrent update є нормальною частиною роботи з конкурентними змінами.
Її не можна просто проігнорувати. Потрібно виконати ROLLBACK, а потім за потреби повторити всю транзакцію.
Якщо повторити тільки один запит, а не всю транзакцію, її логіка може використовувати несумісні дані. Повторна спроба повинна починатися з нового BEGIN і включати всі кроки транзакції.
Repeatable Read надає транзакції стабільний знімок даних.
Усі запити транзакції бачать один і той самий стан бази.
Зміни поточної транзакції їй видимі, а пізніші коміти інших транзакцій — ні.
PostgreSQL може завершити операцію помилкою серіалізації через конкурентне оновлення.
Після такої помилки потрібно виконати ROLLBACK і за потреби повторити всю транзакцію.
Рівень ізоляції слід встановлювати до першого звичайного запиту.
Довгі транзакції слід уникати, щоб не створювати зайве навантаження на базу даних.