Уроки модуля (3/4)
Блокировка и ВЫБОР ДЛЯ ОБНОВЛЕНИЯ
Понять, как база данных работает изолированно на одном терминале, просто… нет, подождите, я уже это говорил!
Иногда «Атомарных обновлений» (например, stock = stock - 1) недостаточно.
Предположим, вам нужно проверить, достаточно ли у пользователя баланса для покупки вещей на сумму 50 евро:
SELECT balance FROM users WHERE id = 1;- JS: Если баланс < 50, заблокируйте их!
- В противном случае продолжите оформление заказа...
- Запустите вычет! (И вот вы находите пользователя, который, проверяя чек на шаге 2, запустил еще один параллельный, который сжег его аккаунт, и теперь вы падаете ниже нуля!).
ДЛЯ ОБНОВЛЕНИЯ Явная блокировка
Добавление FOR UPDATE в конец SELECT волшебным образом превращает ваше невинное чтение в запечатанную блокировку от других клиентов до следующего COMMIT. Никто другой не сможет выполнить UPDATE (или одновременное чтение для обновления) для этих конкретных выбранных строк, пока вы не решите, что с ними делать!
BEGIN;
-- If id=3 had already been locked by T1, T2 will sit in perpetual loading on this "read"
-- frozen in place, before receiving the raw data, until T1 runs COMMIT freeing everyone!
SELECT balance
FROM users
WHERE id = 3
FOR UPDATE;
-- at this point our Node JS instance lives with the guarantee, knowing for certain that NO ONE ELSE in the world has
-- manipulated (nor been able to read with intent to alter) that balance while we complete the application logic
UPDATE users SET balance = balance - 50 WHERE id = 3;
COMMIT;Существует даже FOR SHARE, который позволяет другим читателям продолжать чтение (если используется базовая блокировка), не блокируя чтение состояния, но блокируя любое возможное последующее обновление до тех пор, пока вы не зафиксируете. Однако для внесения изменений всегда используйте грубую силу предложения исключительной блокировки:
Попробуйте сами
Откройте обычную временную транзакцию и заблокируйте исключительно чтение продукта с идентификатором = 5.\nВыполните двухэтапный запрос:\n1. Откройте транзакцию (BEGIN)\n2. Запросите «продукты», получая полную строку для идентификатора 5, с гарантией того, что никто не изменит ее, добавив опцию «ДЛЯ ОБНОВЛЕНИЯ».
Показать подсказку
BEGIN;, новая строка, ВЫБЕРИТЕ * ИЗ продуктов ГДЕ id = 5 ДЛЯ ОБНОВЛЕНИЯ;
Решение доступно после 3 попыток
Иногда транзакция зависает на катастрофические периоды (взаимная блокировка или бесконечное ожидание).\nЧтобы предотвратить это, вы можете дать команду базе данных прекратить работу в тот момент, когда строка не будет доступна немедленно: используйте NOWAIT в конце, что приведет к тому, что исключение SQL-сервера вернется обратно в JS и будет перехвачено без бесконечного зависания.\nЗапустите SELECT для категории ID = 1 и используйте FOR UPDATE, но принудительно включите NOWAIT.
Показать подсказку
ВЫБЕРИТЕ * ИЗ категорий ГДЕ id = 1 ДЛЯ ОБНОВЛЕНИЯ СЕЙЧАС;
Решение доступно после 3 попыток