← Назад к вопросам

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

2.0 Middle🔥 221 комментариев
#Базы данных#Конкурентность и горутины

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

В конкурентной среде, где несколько процессов или потоков могут обращаться к общим данным, блокировки — это механизмы, обеспечивающие согласованность данных. Два основных подхода — оптимистичная (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 оптимистичный подход часто предпочтительнее из-за его масштабируемости.