Ответ
Это два разных подхода к управлению одновременным доступом к данным в транзакционных системах.
Пессимистичная блокировка
Предполагает, что конфликты (попытки нескольких транзакций изменить одни и те же данные) вероятны. Ресурс блокируется до завершения транзакции, чтобы предотвратить доступ других транзакций.
- Как работает: Перед изменением данных на них накладывается эксклюзивная блокировка (например, с помощью
SELECT ... FOR UPDATE
в SQL). - Плюсы: Гарантирует целостность данных, так как конфликты предотвращаются на корню.
- Минусы: Снижает параллельность. Если транзакция долгая, другие транзакции будут простаивать в ожидании снятия блокировки. Может приводить к взаимоблокировкам (deadlocks).
- Когда использовать: В системах с высокой вероятностью конфликтов, где цена простоя ниже цены отката транзакции.
Оптимистичная блокировка
Предполагает, что конфликты маловероятны. Транзакциям разрешается работать с данными без блокировок, а проверка на конфликт происходит в самом конце, перед коммитом.
- Как работает: Используется версионирование. Каждая запись имеет номер версии или временную метку. При обновлении проверяется, что версия записи не изменилась с момента её чтения. Если изменилась — транзакция откатывается.
- Плюсы: Высокая производительность и параллельность, так как нет ожидания на блокировках.
- Минусы: Если конфликты все же происходят, приходится откатывать и повторять транзакции, что может быть накладно.
- Когда использовать: В системах с низкой вероятностью конфликтов и где большинство операций — это чтение.
Пример оптимистичной блокировки в Go:
Идиоматичный способ — проверить количество затронутых строк после UPDATE
.
// Предполагается, что в таблице accounts есть поля id, balance, version
func transfer(db *sql.DB, accountID int, currentVersion int, amount int) error {
// Пытаемся обновить баланс, только если версия совпадает
result, err := db.Exec(
`UPDATE accounts
SET balance = balance + $1, version = version + 1
WHERE id = $2 AND version = $3`,
amount, accountID, currentVersion,
)
if err != nil {
return err
}
// Проверяем, была ли обновлена ровно одна строка
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
if rowsAffected == 0 {
// Если 0, значит, версия устарела или записи не существует.
// Это и есть обнаружение конфликта.
return errors.New("конфликт версий: данные были изменены другой транзакцией")
}
return nil
}