Что делает оператор SELECT FOR UPDATE в SQL и когда его применяют?

Ответ

SELECT ... FOR UPDATE — это SQL-оператор, реализующий пессимистическую блокировку на уровне строк (или таблиц). Он блокирует выбранные строки для других транзакций, пытающихся выполнить SELECT FOR UPDATE, UPDATE или DELETE на этих же строках, до завершения текущей транзакции (COMMIT или ROLLBACK).

Назначение: Предотвращение аномалий параллелизма, таких как "потерянное обновление" (lost update), когда изменения одной транзакции перезаписываются другой.

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

Connection conn = dataSource.getConnection();
try {
    conn.setAutoCommit(false); // Начало транзакции

    // 1. Пессимистическая блокировка строки
    String selectSql = "SELECT id, balance FROM accounts WHERE id = ? FOR UPDATE";
    PreparedStatement selectStmt = conn.prepareStatement(selectSql);
    selectStmt.setLong(1, accountId);
    ResultSet rs = selectStmt.executeQuery();

    if (rs.next()) {
        double currentBalance = rs.getDouble("balance");
        double newBalance = currentBalance - 100.0;

        // 2. Безопасное обновление заблокированной строки
        String updateSql = "UPDATE accounts SET balance = ? WHERE id = ?";
        PreparedStatement updateStmt = conn.prepareStatement(updateSql);
        updateStmt.setDouble(1, newBalance);
        updateStmt.setLong(2, accountId);
        updateStmt.executeUpdate();
    }
    conn.commit(); // Освобождение блокировок
} catch (SQLException e) {
    conn.rollback(); // Освобождение блокировок при ошибке
    throw e;
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

Важные аспекты и подводные камни:

  1. Область действия блокировки: Действует до конца транзакции. Долгие транзакции = долгие блокировки = снижение параллелизма.
  2. Риск взаимных блокировок (deadlock): Возникает, если две транзакции блокируют ресурсы в разном порядке. Решение: всегда захватывать блокировки в согласованном порядке.
  3. Производительность: Пессимистические блокировки снижают параллелизм. Альтернатива — оптимистическая блокировка (через версию или timestamp).
  4. В Spring (@Transactional): Уровень изоляции SERIALIZABLE или использование JdbcTemplate/EntityManager с нативным запросом.
    @Transactional
    public void transferMoney(Long fromId, Long toId, double amount) {
    // Использование JPA с нативным запросом
    entityManager.createNativeQuery("SELECT * FROM accounts WHERE id = :id FOR UPDATE")
                 .setParameter("id", fromId)
                 .getResultList();
    // ... логика обновления
    }

Ответ 18+ 🔞

А, ну вот, блядь, классика жанра! SELECT ... FOR UPDATE, сука! Это как прийти в магазин за последним хлебом, схватить его и орать: «Это моё, ёпта! Кто тронет — в ебальнике получит!». Пессимистическая блокировка, блядь, в чистом виде.

Суть проста, как три копейки: ты говоришь базе — «Слушай сюда, эти строчки сейчас мои, и пока я не скажу „отстаньте, я всё“ (COMMIT) или „ой, нахуй“ (ROLLBACK), ни одна другая транзакция на них даже посмотреть не сможет с таким же запросом, а уж обновить или удалить — вообще забудь».

Зачем это надо? Чтобы не было этой ебаной «потеряшки» (lost update), когда два процесса одновременно читают, что на счету 100 рублей, оба вычитают по 10, и записывают обратно 90. В итоге вместо 80 на счёте — нихуя, 90, потому что второй записал поверх первого. Пиздец и бардак.

Смотри, как это в коде выглядит, на примере перевода бабла:

Connection conn = dataSource.getConnection();
try {
    conn.setAutoCommit(false); // Поехали, блядь, транзакция!

    // 1. Хватаем строчку со счётом и вешаем на неё замок
    String selectSql = "SELECT id, balance FROM accounts WHERE id = ? FOR UPDATE";
    PreparedStatement selectStmt = conn.prepareStatement(selectSql);
    selectStmt.setLong(1, accountId);
    ResultSet rs = selectStmt.executeQuery();

    if (rs.next()) {
        double currentBalance = rs.getDouble("balance");
        double newBalance = currentBalance - 100.0; // Списание, мать его

        // 2. А вот теперь спокойно обновляем, потому что мы тут одни такие умные
        String updateSql = "UPDATE accounts SET balance = ? WHERE id = ?";
        PreparedStatement updateStmt = conn.prepareStatement(updateSql);
        updateStmt.setDouble(1, newBalance);
        updateStmt.setLong(2, accountId);
        updateStmt.executeUpdate();
    }
    conn.commit(); // Всё, отпускаем замки, выдыхаем
} catch (SQLException e) {
    conn.rollback(); // Если всё пошло по пизде, тоже отпускаем
    throw e;
} finally {
    conn.setAutoCommit(true);
    conn.close();
}

Теперь про подводные ебучки, которые тебя ждут:

  1. Долгоиграющие транзакции — это пиздец. Заблокировал строчку и пошёл чай пить на полчаса? Поздравляю, ты только что создал очередь из всех, кто хочет к этой строчке обратиться. Производительность накрылась медным тазом.
  2. Взаимная блокировка (deadlock) — это высший пилотаж. Транзакция А заблокировала строчку 1 и хочет строчку 2. А транзакция Б уже заблокировала строчку 2 и хочет строчку 1. Они сидят и смотрят друг на друга, как два упыря. База через какое-то время одну из них прибьёт с ошибкой. Лечится просто: всегда блокируй ресурсы в одном и том же порядке, ёпта!
  3. Производительность опять. Вечные замки — это не всегда хорошо. Иногда лучше оптимистическая блокировка (версия или timestamp), где ты сначала делаешь, а потом проверяешь, не изменил ли кто-то данные пока ты тут вычислял. Но там свои танцы с бубном.
  4. В Spring, через @Transactional, можно тоже это провернуть. Либо выкрутить уровень изоляции до SERIALIZABLE (но это овердохуища тяжко), либо влезть с нативным запросом.
@Transactional
public void transferMoney(Long fromId, Long toId, double amount) {
    // Берём JPA и тычем ему палкой, чтобы сделал FOR UPDATE
    entityManager.createNativeQuery("SELECT * FROM accounts WHERE id = :id FOR UPDATE")
                 .setParameter("id", fromId)
                 .getResultList();
    // ... а дальше уже делаем что хотели
}

Короче, инструмент мощный, но как молоток: можно гвоздь забить, а можно себе по ебалу получить. Думай, где применять, а то накосячишь — сам от себя охуеешь.