Що таке брудне читання (Dirty Read)
Якщо транзакція, що змінила дані, згодом завершиться помилкою або буде примусово скасована (ROLLBACK), прочитані першою транзакцією дані виявляються хибними («брудними»), оскіль...
🟢 Junior Level
Брудне читання (Dirty Read) — це класична аномалія паралельного доступу до даних, за якої одна транзакція зчитує проміжні зміни, внесені іншою паралельною транзакцією, яка ще не була зафіксована командою COMMIT.
Якщо транзакція, що змінила дані, згодом завершиться помилкою або буде примусово скасована (ROLLBACK), прочитані першою транзакцією дані виявляються хибними («брудними»), оскільки в базі даних їх фактично ніколи не існувало.
Приклад сценарію брудного читання
Час Транзакція А (Поповнення рахунку) Транзакція Б (Видача кредиту)
| --------------------------------- -----------------------------
| T1: BEGIN;
| T2: UPDATE accounts SET balance = 500_000
| WHERE id = 42;
| T3: BEGIN;
| T4: SELECT balance FROM accounts WHERE id = 42;
| -- Прочитав 500 000 грн.! (БРУДНІ ДАНІ)
| T5: -- Схвалив кредит на основі 500 000 грн.
| T6: ROLLBACK; -- Помилка банку!
| Баланс повернувся до 10 000 грн.
v T7: COMMIT; -- Кредит видано банкруту!
На яких рівнях ізоляції можливе
- Read Uncommitted: ⚠️ Єдиний рівень, на якому допускається брудне читання.
- Read Committed, Repeatable Read, Serializable: ❌ Повністю захищені від брудного читання.
Аналогія: Ви зазирнули в чернетку диплома однокурсника й списали собі формулу. Наступного дня викладач вказав на помилку у формулі, і однокурсник закреслив її. Проте ви вже встигли здати роботу з хибними даними.
🟡 Middle Level
Формальне визначення (ANSI SQL vs Berenson et al.)
У класичному стандарті ANSI SQL-92 аномалія $A_1$ (Dirty Read) формулювалася суворо: \(w_1[x] \dots r_2[x] \dots (a_1 \text{ and } c_2 \text{ in any order})\) Транзакція $T_1$ змінює $x$, транзакція $T_2$ зчитує $x$, потім $T_1$ скасовується ($a_1$), а $T_2$ фіксується ($c_2$).
У відомій дослідницькій праці 1995 року («A Critique of ANSI SQL Isolation Levels», Джим Ґрей, Гел Беренсон та ін.) дослідники довели неповноту визначення ANSI та запропонували суворіше феноменологічне правило $P_1$: \(w_1[x] \dots r_2[x] \dots (c_1 \text{ or } a_1)\) Транзакція $T_2$ не повинна читати значення $x$, доки транзакція $T_1$ не завершиться коммітом або абортом.
Як сучасні СУБД захищають від Dirty Read
ЗАХИСТ ВІД DIRTY READ У СУБД
│
┌───────────────────────┴───────────────────────┐
▼ ▼
ПЕСИМІСТИЧНІ БЛОКУВАННЯ (2PL) БАГАТОВЕРСІЙНІСТЬ (MVCC)
- Транзакція запису бере Exclusive Lock (X) - Транзакція запису створює нову версію кортежу
- Транзакція читання вимагає Shared Lock (S) - Версія позначається незавершеним xmin (IN_PROGRESS)
- S-lock конфліктує з X-lock: читач чекає! - Читачі миттєво читають стару версію зі Snapshot
- Приклад: класичний MS SQL Server - Приклад: PostgreSQL, Oracle, MySQL InnoDB
Демонстрація в коді Java / Spring
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountOperationService {
private final AccountRepository accountRepository;
public AccountOperationService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
// Використання Read Committed гарантує захист від брудного читання:
@Transactional(isolation = Isolation.READ_COMMITTED)
public BigDecimal checkVerifiedBalance(Long accountId) {
// Запит фізично не побачить незакомічені апдейти інших потоків
return accountRepository.findBalanceById(accountId);
}
}
🔴 Senior Level
Перевірка видимості на рівні ядра PostgreSQL (HeapTupleSatisfiesMVCC)
У рушії PostgreSQL захист від брудного читання є фундаментальною властивістю структури зберігання рядків у файлах сторінок (Heap Page):
Кожен кортеж рядка зберігає два службові 32-бітні ідентифікатори транзакцій:
t_xmin: ID транзакції, що створила цю версію рядка.t_xmax: ID транзакції, що видалила або оновила цю версію рядка.
Під час виконання будь-якого читання функція HeapTupleSatisfiesMVCC перевіряє статус t_xmin за бітовою картою транзакцій в оперативній пам’яті (pg_xact):
// Спрощений фрагмент вихідного коду PostgreSQL (tqual.c):
if (!TransactionIdDidCommit(tuple->t_xmin)) {
if (TransactionIdIsCurrentTransactionId(tuple->t_xmin)) {
// Бачимо свої власні незакомічені зміни
} else {
// Чужі незакомічені зміни (IN_PROGRESS) -> РЯДОК НЕВИДИМИЙ!
return false;
}
}
Ключовий архітектурний висновок:
В архітектурі PostgreSQL захист від Dirty Read не потребує жодних блокувань пам’яті або очікування звільнення рядків. Незафіксований рядок визнається невидимим на рівні кількох процесорних інструкцій порівняння бітових масок. Саме тому PostgreSQL у принципі не має накладних витрат на запобігання брудному читанню і не реалізує Read Uncommitted.
Каскадні руйнування: чому Dirty Read вважається критичною загрозою
Брудне читання є найбільш руйнівною аномалією, оскільки на відміну від Deadlock або Timeout:
- Воно не генерує помилок (Silent Corruption): Застосунок не падає, не логує warning і вважає прочитані дані абсолютно валідними.
- Ефект доміно (Cascading Data Contamination): \(\text{Брудне читання } \to \text{ Невірне бізнес-рішення } \to \text{ Запис нових даних у БД } \to \text{ Відправка у зовнішні API}\) Навіть якщо СУБД відкотить першу транзакцію, друга транзакція вже встигне переказати гроші через платіжний шлюз або надрукувати накладну на складі.
4 Tricky Questions
1. Якщо транзакція змінює дані і потім читає їх же до виконання COMMIT, чи вважається це «брудним читанням»?
Відповідь: Ні, це не вважається брудним читанням. Читання транзакцією своїх власних незафіксованих змін (Own-XID Visibility) — це штатна і суворо обов’язкова поведінка всіх реляційних СУБД. Транзакція завжди зобов’язана спостерігати результати своїх власних попередніх операцій (Read-Your-Own-Writes). Правило заборони Dirty Read стосується виключно читання проміжних змін чужих паралельних транзакцій.
2. У чому різниця між аномалією Dirty Read ($A_1$) та аномалією Dirty Write ($G_0$)?
Відповідь:
- Dirty Read (Брудне читання): Транзакція $T_2$ читає незафіксовані дані транзакції $T_1$, яка потім відкочується. Допускається лише на рівні
Read Uncommitted. - Dirty Write (Брудний запис): Транзакція $T_2$ перезаписує (UPDATE/DELETE) значення рядка, зміненого незакоміченою транзакцією $T_1$.
Dirty Write категорично заборонений на ВСІХ рівнях ізоляції в УСІХ СУБД (зокрема й на Read Uncommitted). Якби Dirty Write був можливий, СУБД фізично не змогла б виконати
ROLLBACK(незрозуміло, чию версію відновлювати), що призвело б до фатального руйнування цілісності сховища.
3. Чому в MS SQL Server читання з хінтом WITH (NOLOCK) може повернути дубльовані рядки одного й того самого об’єкта?
Відповідь:
Хінт WITH (NOLOCK) вмикає читання без розділюваних блокувань (еквівалент Read Uncommitted).
У рушіях на базі B-Tree сторінок під час інтенсивних паралельних вставок відбувається розщеплення сторінок (Page Split): переповнена 8-кілобайтна сторінка ділиться навпіл, і частина записів переноситься на нову сторінку.
Якщо запит із NOLOCK сканує таблицю за фізичним порядком сторінок (Allocation Order Scan), він може спочатку прочитати рядок на початковій сторінці, а потім, просунувшись далі ланцюжком сторінок, зустріти той самий рядок, перенесений паралельною транзакцією на нову сторінку.
4. Чи може виникнути Dirty Read у мікросервісній архітектурі під час міжсервісної взаємодії через REST API?
Відповідь:
Так, і це одна з найпоширеніших архітектурних проблем!
Якщо Сервіс А відкрив локальну БД-транзакцію @Transactional, змінив статус замовлення на PAID, а потім безпосередньо з тіла транзакції відправив HTTP-запит до Сервісу Б:
- Сервіс Б робить зворотний запит до бази Сервісу А (або читає спільну репліку).
- Або Сервіс Б виконує списання товару на складі, а Сервіс А після повернення відповіді викидає виняток і робить
ROLLBACKсвоєї БД-транзакції! Сервіс Б ухвалив критичне рішення на основі незафіксованого стану Сервісу А. Рішення: Ніколи не викликати зовнішні синхронні API всередині відкритих БД-транзакцій; публікувати події лише після фактичного комміту транзакції (патерн Transactional Outbox).
🎯 Шпаргалка для інтерв’ю
- Сутність Dirty Read: Читання незафіксованих даних паралельної транзакції, яка згодом може зробити
ROLLBACK. - На якому рівні можливе: Тільки на
Read Uncommitted. На всіх інших рівнях (Read Committed,Repeatable Read,Serializable) гарантовано заборонено. - Де відсутнє: У PostgreSQL та Oracle брудного читання немає в принципі (архітектура MVCC автоматично виключає незакомічені кортежі).
- Різниця з Non-Repeatable Read:
- Dirty Read — читаються незакомічені дані, які можуть зникнути.
- Non-Repeatable Read — читаються закомічені дані, але вони змінилися між двома запитами.
- Dirty Write: Заборонений завжди і скрізь (усі СУБД беруть ексклюзивні блокування на запис).
- Головна небезпека: Безсимптомне псування даних (Silent Data Corruption) та ланцюгова реакція помилкових бізнес-рішень.
Червоні прапорці (чого уникати)
- Твердження, що Dirty Read — це коли транзакція читає свої ж щойно змінені рядки (це штатний Own-XID Visibility).
- Твердження, що Dirty Write дозволений на Read Uncommitted (він суворо заборонений усюди).
- Використання Read Uncommitted /
WITH (NOLOCK)у бізнес-критичному коді. - Виклик зовнішніх REST/gRPC сервісів усередині активної транзакції
@Transactional.