В чем разница между пессимистичной и оптимистичной блокировкой?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Разница между оптимистичной и пессимистичной блокировкой
В конкурентной среде, где несколько процессов или потоков могут обращаться к общим данным, блокировки — это механизмы, обеспечивающие согласованность данных. Два основных подхода — оптимистичная (Optimistic Locking) и пессимистичная (Pessimistic Locking) блокировка — кардинально различаются по философии и реализации.
Философская разница: доверие vs. осторожность
Пессимистичная блокировка исходит из предположения, что конфликты при доступе к данным — это правило, а не исключение. Она блокирует ресурс на время всего взаимодействия с ним, предотвращая любые параллельные изменения.
Оптимистичная блокировка, напротив, предполагает, что конфликты редки. Она позволяет нескольким клиентам читать и даже модифицировать данные одновременно, но проверяет наличие конфликта только в момент фиксации изменений (commit). Если конфликт обнаружен — операция откатывается, и клиент должен повторить её.
Техническая реализация
Пессимистичная блокировка
Обычно реализуется на уровне базы данных или системы с использованием механизмов вроде SELECT ... FOR UPDATE в SQL или мьютексов (mutex) в коде.
-- Пример в SQL (PostgreSQL). Блокировка строки на время транзакции.
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Ресурс заблокирован
-- ... выполняем операции ...
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT; -- Блокировка снимается
// Пример на Go с использованием sync.Mutex
package main
import "sync"
type Account struct {
sync.Mutex
balance int
}
func (a *Account) Withdraw(amount int) {
a.Lock() // Пессимистичная блокировка: захватываем мьютекс
defer a.Unlock() // Гарантированно отпускаем в конце
a.balance -= amount
}
Оптимистичная блокировка
Чаще всего реализуется с помощью версионирования (versioning) или меток времени (timestamp). Каждая запись имеет поле version (или timestamp), которое увеличивается при каждом обновлении.
-- Пример таблицы с версией
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL,
version INT DEFAULT 0
);
-- Оптимистичное обновление. Обновляем только если версия не изменилась.
UPDATE products
SET price = 2000, version = version + 1
WHERE id = 5 AND version = 2; -- Версия 2 была прочитана ранее
-- Если число обновлённых строк = 0 -> произошёл конфликт, нужно повторить логику.
// Пример на Go (упрощённо)
package main
import "errors"
type Product struct {
ID int
Price float64
Version int
}
func updateProductOptimistically(repo *ProductRepository, updated *Product) error {
existing, err := repo.Get(updated.ID)
if err != nil {
return err
}
// Проверяем, не изменилась ли версия с момента чтения
if existing.Version != updated.Version {
return errors.New("конфликт версий: данные были изменены другим пользователем")
}
// Инкрементируем версию и сохраняем
updated.Version++
return repo.Save(updated)
}
Ключевые различия в таблице
| Критерий | Пессимистичная блокировка | Оптимистичная блокировка |
|---|---|---|
| Гипотеза | Конфликты часты | Конфликты редки |
| Блокировка данных | Происходит доступно и на долгое время (на всю операцию) | Не происходит вообще или на очень короткое время (в момент проверки версии и коммита) |
| Производительность | Низкая при высокой конкуренции (очереди на блокировку) | Высокая при низкой конкуренции, может деградировать при частых конфликтах |
| Масштабируемость | Плохая | Хорошая |
| Риск взаимной блокировки (deadlock) | Высокий | Отсутствует (нет долгих блокировок) |
| Сценарий использования | Критичные финансовые операции, инвентаризация (последний товар) | Системы с высокой долей чтения, веб-приложения, REST API, системы с version control (например, Git) |
| Реакция на конфликт | Предотвращает его, сериализуя доступ | Обнаруживает и требует повторения операции (retry) |
Выбор стратегии в Go-разработке
Выбор зависит от паттерна доступа к данным:
- Пессимистичная блокировка выбирается, когда стоимость отката транзакции или повторения операции чрезвычайно высока. Например, списание средств со счёта или бронирование последнего места на рейс. В Go для этого используют
sync.Mutex,sync.RWMutex(для сценариев "один пишет, многие читают") или транзакции с эксклюзивными блокировками в БД. - Оптимистичная блокировка идеальна для высоконагруженных систем, где операции чтения значительно превосходят операции записи, и вероятность одновременного изменения одних и тех же данных мала. Часто применяется в RESTful API, где клиент получает версию ресурса (например, через заголовок
ETag), а при обновлении отправляет её обратно для проверки. Реализуется через поляversionв моделях данных и стратегию повторных попыток (retry).
Важный нюанс для Go: При использовании оптимистичной блокировки в распределённых системах или микросервисах важно реализовать корректную логику повторных попыток (например, с экспоненциальной задержкой — exponential backoff), которая может быть легко построена с помощью цикла for или пакетов вроде github.com/cenkalti/backoff/v4.
Таким образом, пессимистичная блокировка — это стратегия "заблокировать, потом изменить", которая платит производительностью за гарантии. Оптимистичная блокировка — это стратегия "прочитать-изменить-проверить", которая платит сложностью обработки конфликтов за высокий параллелизм. В современных распределённых приложениях на Go оптимистичный подход часто предпочтительнее из-за его масштабируемости.