Ответ
Конструкция SELECT FOR UPDATE используется для пессимистической блокировки строк в транзакции. В PostgreSQL, который часто используется с Go, доступны следующие опции для управления поведением блокировки:
-
NOWAIT- Назначение: Немедленно вернуть ошибку, если запрашиваемые строки уже заблокированы другим процессом. Это полезно, когда ожидание нежелательно.
- SQL:
SELECT * FROM products WHERE id = 1 FOR UPDATE NOWAIT;
-
SKIP LOCKED- Назначение: Пропустить уже заблокированные строки и вернуть только те, которые удалось заблокировать. Идеально подходит для реализации очередей задач, где несколько воркеров могут параллельно разбирать задачи из одной таблицы.
- SQL:
SELECT * FROM tasks WHERE status = 'new' LIMIT 10 FOR UPDATE SKIP LOCKED;
-
FOR NO KEY UPDATE- Назначение: Более слабая блокировка, чем
FOR UPDATE. Она блокирует строку от изменений (UPDATE,DELETE), но не препятствует другим транзакциям выполнятьSELECT FOR KEY SHARE. Используется, когда нужно предотвратить изменение данных, но не блокировать чтение по внешним ключам.
- Назначение: Более слабая блокировка, чем
-
FOR SHARE/FOR KEY SHARE- Назначение: Это разделяемые (shared) блокировки.
FOR SHAREпредотвращает изменение строки другими транзакциями, но позволяет им читать её сFOR SHARE.FOR KEY SHARE— ещё более слабая блокировка, используемая для проверки ссылочной целостности.
- Назначение: Это разделяемые (shared) блокировки.
Пример использования в 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) // Фиксируем транзакцию Ответ 18+ 🔞
Да ты послушай, что за дичь творится с этими блокировками в базе! Сидишь ты такой, пишешь свой код, и тут бац — два процесса одновременно лезут в одну строку, как два мудака в одну дверь. Итог? Пиздец, а не данные. Вот для этого и придумали SELECT FOR UPDATE, чтоб такого не было.
Вот смотри, в Постгресе, с которым мы на Go обычно и работаем, есть целая куча опций, чтобы управлять этим цирком. Запоминай, а то потом будешь сидеть и чесать репу.
1. NOWAIT
Это как попытаться пролезть без очереди в переполненный автобус. Если строка уже кем-то прихвачена, он тебе сразу в лоб ошибкой: «Не, браток, занято, иди нахуй, ждать не буду».
SELECT * FROM products WHERE id = 1 FOR UPDATE NOWAIT;
2. SKIP LOCKED
А это уже похитрее. Видишь, что строка заблокирована? Да и хуй с ней, проходим мимо, как мимо спящего бомжа. Берём только то, что свободно. Идеально для очередей задач, где воркеры как голодные псы рвут куски из одной миски.
SELECT * FROM tasks WHERE status = 'new' LIMIT 10 FOR UPDATE SKIP LOCKED;
3. FOR NO KEY UPDATE
Блокировка послабее. Ты говоришь: «Ребят, я тут буду менять строку, но вы можете читать её по своим внешним ключам, я не жадный». Как будто повесил табличку «Не входить», но оставил форточку открытой.
4. FOR SHARE / FOR KEY SHARE
Это уже вообще почти вежливость. «Я просто почитаю, ребята, вы не меняйте её пока, ладно?». FOR KEY SHARE — это вообще шепотом: «Я только целостность ссылок проверю и всё».
А теперь, блядь, как это в Go выглядит (с драйвером pgx):
Главное помни — вся эта магия работает ТОЛЬКО внутри транзакции, а то будешь как дурак тыкаться.
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 {
// Если использовал NOWAIT и не повезло — получишь ошибку, что блокировка не удалась
return
}
// ... Тут твоя бизнес-логика, считай, меняй, что хочешь ...
_, err = tx.Exec(ctx, "UPDATE products SET quantity = $1 WHERE id = $2", newQuantity, productID)
if err != nil {
return
}
err = tx.Commit(ctx) // И только теперь — бабах! — фиксируем. Всё, строка снова свободна.
Вот и вся наука. Главное — не забудь транзакцию закрыть, а то залочишь всё нахер и будешь потом гадать, почему всё встало колом. Удивление пиздец будет.