Какие опции можно использовать с `SELECT FOR UPDATE` и для чего они нужны?

Ответ

Конструкция SELECT FOR UPDATE используется для пессимистической блокировки строк в транзакции. В PostgreSQL, который часто используется с Go, доступны следующие опции для управления поведением блокировки:

  1. NOWAIT

    • Назначение: Немедленно вернуть ошибку, если запрашиваемые строки уже заблокированы другим процессом. Это полезно, когда ожидание нежелательно.
    • SQL: SELECT * FROM products WHERE id = 1 FOR UPDATE NOWAIT;
  2. SKIP LOCKED

    • Назначение: Пропустить уже заблокированные строки и вернуть только те, которые удалось заблокировать. Идеально подходит для реализации очередей задач, где несколько воркеров могут параллельно разбирать задачи из одной таблицы.
    • SQL: SELECT * FROM tasks WHERE status = 'new' LIMIT 10 FOR UPDATE SKIP LOCKED;
  3. FOR NO KEY UPDATE

    • Назначение: Более слабая блокировка, чем FOR UPDATE. Она блокирует строку от изменений (UPDATE, DELETE), но не препятствует другим транзакциям выполнять SELECT FOR KEY SHARE. Используется, когда нужно предотвратить изменение данных, но не блокировать чтение по внешним ключам.
  4. FOR SHARE / FOR KEY SHARE

    • Назначение: Это разделяемые (shared) блокировки. FOR SHARE предотвращает изменение строки другими транзакциями, но позволяет им читать её с FOR SHARE. FOR KEY SHARE — ещё более слабая блокировка, используемая для проверки ссылочной целостности.

Пример использования в Go (с pgx):

Важно помнить, что FOR UPDATE работает только внутри транзакции.

tx, err := conn.Begin(ctx)
if err != nil {
    // обработка ошибки
}
defer tx.Rollback(ctx) // Откатываем транзакцию в случае ошибки

var productName string
var quantity int

// Блокируем строку товара, чтобы безопасно обновить остаток
err = tx.QueryRow(ctx, 
    "SELECT name, quantity FROM products WHERE id = $1 FOR UPDATE", productID).
    Scan(&productName, &quantity)

if err != nil {
    // Обработка ошибки (включая pgerrcode.LockNotAvailable если используется NOWAIT)
    return
}

// ... логика изменения данных ...

_, err = tx.Exec(ctx, "UPDATE products SET quantity = $1 WHERE id = $2", newQuantity, productID)
if err != nil {
    return
}

err = tx.Commit(ctx) // Фиксируем транзакцию