В документации PostgreSQL сказано: Read Committed is the default isolation level in PostgreSQL. When a transaction uses this isolation level, a SELECT query (without a FOR UPDATE/SHARE clause) sees only data committed before the query began it never sees either uncommitted data or changes committed during query execution by concurrent transactions. In effect, a SELECT query sees a snapshot of the database as of the instant the query begins to run. (…) Я понимаю это так, что если я в транзакции без явного указания уровня изоляции сделаю: SELECT "id" FROM "table" WHERE /* … */ Потом обработаю результаты в приложении, а потом: UPDATE "table" SET "flag" = TRUE WHERE "id" IN (/* … */) То получу нужный результат, а именно обновление всех строк с нужными ID, вне зависимости от того, что там удаляют или изменяют другие транзакции. Но судя по тому, что существует такая вещь как SELECT /* … */ FOR UPDATE, это не так? В каких случаях использование SELECT /* … */ FOR UPDATE необходимо? Как на это влияет уровень изоляции транзакций? Нужен ли SELECT /* … */ FOR UPDATE на уровне SERIALIZABLE?
Ответ Пока у вас транзакции следуют строго одна за другой - у вас всё хорошо и нет проблем. Кроме производительности. А чтобы улучшить производительность - необходимо разрешить транзакции выполнять параллельно. И вот тут начинается богатый и поразительный мир concurrently control. При том, не только в базах данных, а везде где хоть что-то выполняется параллельно. На деньгах люди обычно лучше понимают, так что будем говорить про деньги. Допустим есть пользователь, у него есть 100 денег на счету. Пользователь может их тратить, вы проверяете баланс select balance ..., затем обновляете баланс при покупке update ... set balance = ? where .... И вот в счастливый день как-то так вышло, что приходят сразу два запроса на покупки для этого пользователя. Одна на 50 денег, вторая на 70. Одна из них должна быть отклонена, т.к. денег недостаточно. Но в результате получается что обе покупки прошли и у вас проблема, вы продали то что не надо было. И это даже не видно по балансу пользователя. Как же? Это типичный race condition, обе транзакции сначала данные читают, потом локально что-то делают, потом что-то пишут. читать им никто не мешает, потому обе транзакции прочитали что у пользователя 100 денег обе транзакции закономерно решили что денег достаточно обе транзакции обновили баланс пользователя На конкурентном доступе к ресурсу подрались только на последнем шаге, транзакция которая начала обновлять данные позже сначала подождала завершение первой транзакции. А затем банально перезаписала баланс на тот который считала правильным сама. Так называемая lost update аномалия. То есть, в существующем виде эти две транзакции были неверно сериализованы. Для корректного выполнения логики вторая пришедшая транзакция должна была подождать результат первой транзакции до чтения баланса пользователя. Но базу данных об этом не предупредили, и вполне закономерно этот первый select был расценен как не мешающий другим. Вот как раз для того чтобы предупредить СУБД о том, что мы планируем с данными что-то делать, а потому нам надо сериализовать транзакции иначе, и существует FOR SHARE, FOR UPDATE дополнения. Потому они кстати и задокументированы в разделе Explicit Locking Если ничего не указано в select - то из-за сущности MVCC реализации postgresql транзакции смогут читать данные чуть менее чем всегда. Даже если прямо сейчас другая транзакция эту строку уже обновляет - мы получим последнее известное зафиксированное значение этой строк. если запросить явно FOR SHARE - то читать мы сможем в много потоков с этой FOR SHARE блокировкой не блокируя друг друга. Но вот если кто-то захочет обновить эту строку - то он встанет в очередь ожидания пока не завершатся все транзакции удерживающие читающую блокировку и вместе с тем задержит все последующие FOR SHARE транзакции. если запросить FOR UPDATE - то мы можем быть уверены, что ни одна другая транзакция не сможет обновить эту строку до конца нашей транзакции. То есть нужны в тех местах, где без этого конкурентная транзакция может сериализоваться логично с точки зрения СУБД, но некорректно для бизнес логики приложения. Serializable Serializable transactions are just Repeatable Read transactions which add nonblocking monitoring for dangerous patterns of read/write conflicts Реализация Serializable в postgresql отслеживает изменения которые повлекут нарушение изоляции транзакций и все подверженные транзакции получат ошибку сериализации. Само приложение должно быть готово работать на этом уровне изоляции и предполагать получить ошибку сериализации в любой момент времени, быть готовым повторить транзакцию заново. Нужен ли здесь FOR UPDATE - просто процитирую документацию с советами по уменьшению проседания производительности от Serializable уровня изоляции: Eliminate explicit locks, SELECT FOR UPDATE, and SELECT FOR SHARE where no longer needed due to the protections automatically provided by Serializable transactions. На этом уровне изоляции явные блокировки не нужны.
в транзакции без явного указания уровня изоляции Будет использован уровень указанный в настройке default_transaction_isolation. То есть это может быть и Serializable вместо Read Committed. получу нужный результат, а именно обновление всех строк с нужными ID, вне зависимости от того, что там удаляют или изменяют другие транзакции. Вы получите обновление всех строк попадающих под условие. А вот нужен ли именно этот результат - вот ключевой вопрос.