Уроки модуля (2/4)
Условия гонки
Понять, как база данных работает изолированно на одном терминале, просто, но в реальном веб-сценарии будут сотни одновременных процессов «Node/PHP/Go», отправляющих запросы. Именно здесь рождаются знаменитые условия гонки.
Проблема «потерянного обновления»
Потерянное обновление происходит, когда два потока читают одну и ту же старую строку, выполняют некоторые математические вычисления на сервере JavaScript, а затем отправляют обратно UPDATE, чтобы сохранить результат.
Сценарий:
- Товар #5 имеет 10 единиц.
- Пользователь А покупает один (Сервер А читает «10», вычисляет «9»).
- За ту же миллисекунду пользователь B покупает еще один, прежде чем покупка A завершит фазу COMMIT.
- Сервер B также читает «10», вычисляет «9».
- Сервер А отправляет
UPDATE: set quantity = 9. - Сервер Б отправляет
UPDATE: set quantity = 9. - Оба думают, что успешно уменьшили это число. Но итоговое значение должно было составить 8 единиц! Фантомная покупка потеряна.
Решение без тяжелых транзакций (Atomic Updates)
Вместо того, чтобы читать и выполнять математические вычисления снаружи, позвольте БД делать это во время самого обновления, которое всегда синхронизируется.
-- Wrong
UPDATE products SET stock = 9 WHERE id = 5;
-- Correct and natively thread-safe
UPDATE products SET stock = stock - 1 WHERE id = 5;Более экстремальные аномалии (грязные чтения и фантомы)
- Грязное чтение: вы читаете чужие временные данные, которые еще не подтверждены COMMIT (и могут внезапно перейти к ROLLBACK, сделав вас недействительными).
- Фантомное чтение: вы сканируете все строки, в то время как второй поток вставляет новую запись, затем вы выполняете еще одно сканирование во время той же транзакции и волшебным образом обнаруживаете, что новая запись появилась из ниоткуда, ставя под угрозу рассчитанные финансовые отчеты.
Атомарно измените запись о клиентах для небольшой манипуляции со строками.\nПредположим, мы хотим переименовать «город» клиента, чтобы добавить звездочку. \nДелайте это, используя собственное значение строки для строки с id=10, объединяя '- Old' с текущим значением (используйте конвейер конкатенации postgres ||).
Показать подсказку
ОБНОВЛЕНИЕ клиентов SET city = city || '- Старый' ГДЕ id = 10;
Решение доступно после 3 попыток
Классическое потерянное обновление происходит в платежах и ценах.\nПредположим, произошло экстренное повышение цен: увеличьте 'unit_price' таблицы 'order_items' (для продукта 1 заказа 1) на 20% атомарно, т. е. умножив ее * 1,2 вместо вставки новой цены, предварительно рассчитанной вручную в JS.
Показать подсказку
ОБНОВЛЕНИЕ order_items SET unit_price = unit_price * 1.2 ГДЕ order_id = 1 И product_id = 1;
Решение доступно после 3 попыток