Пошук уроків, статей та іншого контенту
Два підходи до захисту від одночасного редагування одного й того самого рядка кількома користувачами.
Двоє користувачів відкривають той самий запис одночасно, обидва вносять зміни й зберігають — без жодного захисту зміни того, хто зберіг другим, мовчки перезапишуть зміни першого (класична проблема lost update). Транзакції самі по собі (стаття «Транзакції: атомарність операцій» курсу PostgreSQL) гарантують атомарність кожного окремого запиту, але не координують два незалежні запити користувачів, що не знають одне про одного.
Песимістичне блокування виходить з припущення, що конфлікти ймовірні, і блокує рядок фізично на весь час, доки одна транзакція його редагує — інші транзакції, що намагаються заблокувати той самий рядок, чекають:
BEGIN;
SELECT * FROM accounts WHERE id = 42 FOR UPDATE; -- блокує рядок для інших SELECT ... FOR UPDATE
-- ...застосунок читає баланс, обчислює новий...
UPDATE accounts SET balance = balance - 100 WHERE id = 42;
COMMIT; -- блокування знімаєтьсяFOR UPDATE гарантує, що жодна інша транзакція не зможе одночасно прочитати цей рядок із наміром його змінити, доки поточна транзакція не завершиться — конфлікт стає фізично неможливим ціною того, що інші учасники чекають.
Оптимістичне блокування виходить з протилежного припущення — конфлікти рідкісні, тож не варто платити за блокування щоразу. Замість цього до таблиці додають поле-версію (version або updated_at), яке зчитують разом із рядком, а при збереженні перевіряють, чи воно не змінилось відтоді:
-- Зчитали рядок разом із поточною версією: version = 5
UPDATE accounts
SET balance = 400, version = version + 1
WHERE id = 42 AND version = 5; -- умова спрацює, лише якщо версія не змінилась
-- Якщо хтось інший устиг оновити рядок першим (version уже 6),
-- WHERE не знайде жодного рядка — застосунок бачить 0 оновлених рядків
-- і повідомляє користувачу про конфлікт замість тихого перезаписуЯкщо запит оновив 0 рядків — застосунок знає, що дані змінились між читанням і записом, і може або показати користувачу помилку «дані застаріли, оновіть сторінку», або спробувати злити зміни автоматично, залежно від сценарію.
Pessimistic locking — коли конфлікти частi, а вартість повторної спроби висока (банківські перекази, бронювання останнього місця на рейс): краще змусити другого користувача почекати, ніж давати йому робити роботу, яку доведеться відкинути.
Optimistic locking — коли конфлікти рідкісні, а більшість запитів на читання/запис не перетинаються (редагування профілю користувача, CMS-контент): не варто платити вартість блокування там, де воно майже ніколи не знадобиться.
Pessimistic locking має реальну вартість пропускної здатності: заблокований рядок змушує чекати всі інші транзакції, що хочуть його змінити, а за неправильного порядку блокувань кількох рядків можна отримати deadlock — дві транзакції, що чекають одна на одну назавжди.
Використовувати SELECT FOR UPDATE «про всяк випадок» на таблицях із високим навантаженням на читання-запис — це штучно серіалізує операції, які насправді рідко конфліктують, і зменшує пропускну здатність без реальної потреби.
Реалізувати оптимістичне блокування, але ігнорувати випадок 0 оновлених рядків (мовчки вважати оновлення успішним) — це прибирає сенс перевірки версії: конфлікт технічно виявлено, але застосунок про нього так і не дізнається.
Тримати транзакцію з FOR UPDATE відкритою довше, ніж потрібно (наприклад, чекаючи на повільний зовнішній API-виклик усередині транзакції) — блокування тримається весь цей час, і всі інші транзакції на той самий рядок чекають разом з ним.
Pessimistic locking фізично блокує рядок на час редагування (SELECT ... FOR UPDATE), гарантуючи відсутність конфлікту ціною очікування інших транзакцій — підходить, коли конфлікти частi й дорогі. Optimistic locking не блокує нічого заздалегідь, а перевіряє поле-версію перед записом і відхиляє оновлення, якщо дані вже змінились, — дешевше в звичайному випадку, але вимагає явної обробки конфлікту в коді застосунку.