Skip to main content

ACID - consistency

ACID Consistency (узгодженість) - гарантія бази даних, що кожна транзакція переводить дані з одного валідного стану в інший, дотримуючись усіх правил і обмежень.

Теорія

TL;DR

  • Уяви правило банківського сейфа: або обидва рахунки оновлюються повністю під час переказу, або нічого не змінюється.
  • БД перевіряє всі constraints у момент COMMIT. Будь-яке порушення - автоматичний ROLLBACK.
  • Consistency = твої бізнес-правила (balance >= 0). Isolation - окрема концепція, захист від одночасних змін.
  • Правила переноси на рівень БД (CHECK, FK, тригери). Логіку в додатку - тільки для складних мульти-БД сценаріїв.

Швидкий приклад

sql
-- PostgreSQL: enforce balance >= 0 на рівні БД CREATE TABLE accounts ( id SERIAL PRIMARY KEY, owner TEXT NOT NULL, balance NUMERIC(12,2) NOT NULL CHECK (balance >= 0) ); INSERT INTO accounts (owner, balance) VALUES ('Alice', 100); BEGIN; UPDATE accounts SET balance = balance - 150 WHERE owner = 'Alice'; -- спроба -50 COMMIT; -- FAILS: CHECK constraint, авто-ROLLBACK SELECT balance FROM accounts WHERE owner = 'Alice'; -- все ще 100

Баланс Alice залишається 100. Транзакція відкотилась автоматично, бо -50 порушує CHECK. Жодного проміжного стану, жодного негативного балансу.

Ключова відмінність

Consistency у ACID відрізняється від звичайної валідації тим, що прив'язана до транзакції атомарно. Перевірка в коді додатку може програти в гонці або впасти між читанням і записом - це TOCTOU (time-of-check-to-time-of-update). Обмеження на рівні БД блокують COMMIT доти, поки всі правила не виконані по всій транзакції. Це виключає ситуації, де часткове оновлення ламає інваріанти системи.

Коли що використовувати

  • Balance >= 0, NOT NULL, формати полів - CHECK constraints у БД (швидко, безпечно при конкурентному доступі)
  • Правила між таблицями (загальний борг <= кредитний ліміт) - тригери або stored procedures
  • Кілька БД або зовнішні системи - saga pattern або 2PC у додатку
  • High-scale NoSQL - логіка в додатку + eventual consistency (компроміс BASE)

Як PostgreSQL перевіряє узгодженість

PostgreSQL перевіряє узгодженість у момент коміту транзакції. БД блокує рядки, що змінились, перевіряє всі constraints і тригери проти фінального набору записів і тільки якщо все пройшло - записує в WAL. Якщо будь-який CHECK, FK або UNIQUE впав, вся транзакція відкочується через MVCC.

Перевірка constraints відбувається після всіх записів, але до коміту. Відкладені constraints (SET CONSTRAINTS ALL DEFERRED) чекають до COMMIT - це потрібно для кругових FK або багатокрокових переказів, де проміжний стан тимчасово невалідний.

Після краша WAL replay відновлює закомічені транзакції в порядку коміту і відкочує незавершені. Жоден частковий стан не пережить відновлення.

Типові помилки

  1. Валідація тільки в коді додатку
sql
-- WRONG: додаток читає баланс, потім окремо оновлює IF (SELECT balance FROM accounts WHERE id = 1) > 100 THEN UPDATE accounts SET balance = balance - 100 WHERE id = 1; END IF; -- Конкурентна транзакція може списати кошти між перевіркою і UPDATE

Дві транзакції можуть пройти перевірку одночасно і обидві списати гроші. CHECK constraint у БД закриває це, перевіряючи в момент коміту з правильними блокуваннями.

  1. READ COMMITTED там, де потрібна строга узгодженість

READ COMMITTED допускає non-repeatable reads. Транзакція може прочитати той самий рядок двічі і отримати різні значення, якщо між читаннями зафіксувалась інша транзакція. Для інваріантів, що охоплюють кілька читань (наприклад, перевірка унікальності email перед вставкою), використовуй SERIALIZABLE.

  1. Immediate constraints при багатокрокових операціях
sql
-- WRONG для двоетапного переказу: SET CONSTRAINTS ALL IMMEDIATE; -- Дебет спрацює CHECK до того, як відбудеться кредит. Впаде на проміжному стані. -- RIGHT: SET CONSTRAINTS ALL DEFERRED; -- CHECK перевіряється тільки на COMMIT, після всіх записів.
  1. Тригери без BEFORE контексту

RAISE EXCEPTION в AFTER тригері в деяких сценаріях спрацьовує запізно. Для логіки обмежень використовуй BEFORE тригери, щоб відкат стався до запису рядка.

Де зустрічається на практиці

  • PostgreSQL + Rails: ActiveRecord огортає моделі в транзакції і використовує CHECK/FK. Shopify побудований на цьому підході.
  • CockroachDB: distributed SQL забезпечує serializable consistency через total order broadcast.
  • Google Spanner: TrueTime API для зовнішньої узгодженості в гео-розподілених транзакціях.
  • MySQL InnoDB: gap locks блокують phantom reads у REPEATABLE READ режимі.
  • Компроміс NoSQL: DynamoDB і Cassandra обирають доступність (BASE). ACID потрібен для грошей та інвентарю, eventual consistency підходить для логів і стрічок новин.

Питання на співбесіді

Q: Яка різниця між Consistency і Isolation у ACID?
A: Consistency перевіряє бізнес-правила (інваріанти). Isolation приховує конкурентні зміни від поточної транзакції. Consistency падає через погані дані, Isolation - через гонки.

Q: Як працює consistency в розподіленій системі типу Google Spanner?
A: Spanner використовує TrueTime для зовнішньої узгодженості. Коміти отримують мітки часу так, що якщо Tx1 завершився до початку Tx2, Tx2 гарантовано бачить результати Tx1. 2PC не потрібен.

Q: Чому транзакція може пройти всі constraints локально, але впасти на коміті?
A: Відкладені constraints і тригери перевіряються після всіх записів, але до коміту. Конкурентна транзакція може видалити FK-ціль або порушити UNIQUE у вікні між твоїми записами і комітом.

Q: Розкажи, як WAL replay забезпечує узгодженість після краша в PostgreSQL.
A: WAL зберігає повні образи транзакцій. При відновленні replay застосовує закомічені транзакції в порядку коміту і відкочує незавершені. Жоден частковий стан не може пережити цей процес.

Q: Що таке TOCTOU-гонка і чому вона ламає узгодженість на рівні додатку?
A: TOCTOU - проміжок між читанням значення і записом на його основі. Конкурентна транзакція може змінити значення в цьому проміжку. DB constraints усувають це, перевіряючи в момент коміту під блокуваннями.

Приклади

Базовий: constraint блокує невалідну транзакцію

sql
CREATE TABLE accounts ( id SERIAL PRIMARY KEY, owner TEXT NOT NULL, balance NUMERIC(12,2) NOT NULL CHECK (balance >= 0) ); INSERT INTO accounts (owner, balance) VALUES ('Alice', 100), ('Bob', 10); BEGIN; UPDATE accounts SET balance = balance - 150 WHERE owner = 'Alice'; -- -50 COMMIT; -- ERROR: new row violates check constraint "accounts_balance_check" -- Баланс Alice все ще 100

Транзакція ніколи не закомітилась. PostgreSQL знайшов порушення обмеження і відкотив усе автоматично. Код додатку нічого не робив.

Середній: атомарний переказ коштів

sql
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE owner = 'Alice' RETURNING balance; -- 0 після цього UPDATE accounts SET balance = balance + 100 WHERE owner = 'Bob' RETURNING balance; -- 110 після цього COMMIT; -- Результат: Alice = 0, Bob = 110 -- Якщо будь-який UPDATE впав, обидва відкочуються

Це паттерн, який Stripe використовує для коригування балансів разом з idempotency keys для безпечних повторних спроб. Або обидва записи проходять, або жоден. Ніколи не буде стану, де Alice втратила гроші, а Bob їх не отримав.

Складний: багаторівнева узгодженість з тригерами

sql
-- Оптимістичне блокування через версіонування CREATE TABLE wallets ( id SERIAL PRIMARY KEY, balance NUMERIC CHECK (balance >= 0), version INT DEFAULT 0 ); CREATE FUNCTION check_version() RETURNS TRIGGER AS $$ BEGIN IF NEW.version != OLD.version THEN RAISE EXCEPTION 'Concurrent update detected'; END IF; NEW.version := OLD.version + 1; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER version_check BEFORE UPDATE ON wallets FOR EACH ROW EXECUTE FUNCTION check_version();

Якщо дві транзакції прочитали version = 0 і обидві намагаються оновити, одна впаде на перевірці версії. Якщо інша намагається списати більше, ніж є на балансі, CHECK зупинить її. Два незалежних шари: тригер версії блокує конкурентні записи, CHECK блокує невалідний стан. Я бачив, як розробники прибирали CHECK, думаючи що тригер вистачить. Це не так. Тригер закриває гонки, CHECK закриває погані дані. Потрібні обидва.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?