В чем разница между пессимистичной и оптимистичной блокировками?

Ответ

Это два разных подхода к управлению одновременным доступом к данным в транзакционных системах.

Пессимистичная блокировка

Предполагает, что конфликты (попытки нескольких транзакций изменить одни и те же данные) вероятны. Ресурс блокируется до завершения транзакции, чтобы предотвратить доступ других транзакций.

  • Как работает: Перед изменением данных на них накладывается эксклюзивная блокировка (например, с помощью 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
}