Мэт и Эрвин оба правы, и я только добавляю еще один ответ, чтобы еще больше расширить сказанное, что не вписывается в комментарий. Так как их ответы, кажется, не удовлетворяют всех, и было предложение, чтобы консультации с разработчиками PostgreSQL, и я один, я уточню.
Важным моментом здесь является то, что в соответствии со стандартом SQL в транзакции, выполняемой на READ COMMITTED
уровне изоляции транзакции, ограничение заключается в том, что работа незафиксированных транзакций не должна быть видимой. Когда работа совершенных транзакций становится видимой, это зависит от реализации. То, на что вы указываете, - это разница в том, как два продукта выбрали для реализации этого. Ни одна из реализаций не нарушает требования стандарта.
Вот что происходит внутри PostgreSQL, подробно:
S1-1 работает (1 строка удалена)
Старая строка остается на месте, потому что S1 может все еще откатываться, но теперь S1 удерживает блокировку строки, так что любой другой сеанс, пытающийся изменить строку, будет ждать, чтобы увидеть, фиксирует ли S1 или откатывает назад. Любые чтения таблицы все еще могут видеть старую строку, если они не пытаются заблокировать ее с помощью SELECT FOR UPDATE
или SELECT FOR SHARE
.
S2-1 работает (но блокируется, так как S1 имеет блокировку записи)
S2 теперь нужно ждать, чтобы увидеть результат S1. Если S1 выполнить откат вместо фиксации, S2 удалит строку. Обратите внимание, что если S1 вставил новую версию перед откатом, новая версия никогда не была бы там с точки зрения какой-либо другой транзакции, и при этом старая версия не была бы удалена с точки зрения любой другой транзакции.
S1-2 работает (вставлен 1 ряд)
Этот ряд не зависит от старого. Если бы произошло обновление строки с id = 1, старая и новая версии были бы связаны, и S2 мог бы удалить обновленную версию строки, когда она стала разблокированной. То, что новая строка имеет те же значения, что и некоторая строка, существовавшая в прошлом, не делает ее такой же, как обновленная версия этой строки.
S1-3 запускается, снимая блокировку записи
Таким образом, изменения S1 сохраняются. Один ряд ушел. Один ряд был добавлен.
S2-1 запускается, теперь, когда он может получить блокировку. Но отчеты 0 строк удалены. HUH ???
Что происходит внутри, так это то, что существует указатель от одной версии строки к следующей версии этой же строки, если она обновляется. Если строка удалена, следующей версии нет. Когда READ COMMITTED
транзакция пробуждается из блока в конфликте записи, она следует этой цепочке обновлений до конца; если строка не была удалена и если она все еще удовлетворяет критериям выбора запроса, она будет обработана. Эта строка была удалена, поэтому запрос S2 продолжается.
S2 может или не может попасть в новую строку во время сканирования таблицы. Если это произойдет, он увидит, что новая строка была создана после DELETE
запуска оператора S2 , и поэтому не является частью набора строк, видимых для него.
Если PostgreSQL перезапустит весь оператор DELETE S2 с самого начала с новым снимком, он будет вести себя так же, как SQL Server. Сообщество PostgreSQL не решило сделать это по соображениям производительности. В этом простом случае вы бы никогда не заметили разницу в производительности, но если бы вы были на десять миллионов строк, DELETE
когда вас заблокировали, вы бы наверняка заметили . Здесь есть компромисс, когда PostgreSQL выбрал производительность, поскольку более быстрая версия по-прежнему соответствует требованиям стандарта.
S2-2 запускается, сообщает о нарушении ограничения уникального ключа
Конечно, ряд уже существует. Это наименее удивительная часть картины.
Несмотря на то, что здесь наблюдается некоторое удивительное поведение, все соответствует стандарту SQL и находится в пределах того, что является «специфичным для реализации» согласно стандарту. Конечно, это может быть удивительно, если вы предполагаете, что поведение какой-то другой реализации будет присутствовать во всех реализациях, но PostgreSQL очень старается избежать ошибок сериализации на READ COMMITTED
уровне изоляции и допускает некоторые поведения, которые отличаются от других продуктов для достижения этого.
Лично я не большой поклонник READ COMMITTED
уровня изоляции транзакций в реализации любого продукта. Все они позволяют условиям гонки создавать удивительное поведение с точки зрения транзакций. Когда кто-то привыкает к странному поведению, допускаемому одним продуктом, он склонен считать «нормальным» и компромиссы, выбранные другим продуктом, странными. Но каждый продукт должен сделать своего рода компромисс для любого режима, который на самом деле не реализован SERIALIZABLE
. Разработчики PostgreSQL решили провести черту, READ COMMITTED
чтобы минимизировать блокировку (чтение не блокирует запись, а запись не блокирует чтение) и минимизировать вероятность ошибок сериализации.
Стандарт требует, чтобы SERIALIZABLE
транзакции были по умолчанию, но большинство продуктов не делают этого, потому что это вызывает снижение производительности по сравнению с более слабыми уровнями изоляции транзакций. Некоторые продукты даже не предоставляют действительно сериализуемые транзакции при SERIALIZABLE
выборе, особенно Oracle и версии PostgreSQL до 9.1. Но использование подлинных SERIALIZABLE
транзакций - это единственный способ избежать неожиданных последствий условий гонки, и SERIALIZABLE
транзакции всегда должны либо блокироваться, чтобы избежать условий гонки, либо откатывать некоторые транзакции, чтобы избежать развивающейся ситуации гонки. Наиболее распространенной реализацией SERIALIZABLE
транзакций является строгая двухфазная блокировка (S2PL), которая имеет как сбои блокировки, так и сериализации (в форме взаимоблокировок).
Полное раскрытие: я работал с Дэном Портсом из MIT, чтобы добавить действительно сериализуемые транзакции в PostgreSQL версии 9.1, используя новую технику, называемую Serializable Snapshot Isolation.
READ COMMITTED
транзакции, у вас есть условие гонки: что произойдет, если другая транзакция вставит новую строку после первогоDELETE
запуска и до начала второйDELETE
? С транзакциями менее строгими, чемSERIALIZABLE
два основных способа закрытия условий гонки, - это продвижение конфликта (но это не помогает, когда строка удаляется) и материализация конфликта. Вы могли материализовать конфликт, имея таблицу «id», которая была обновлена для каждой удаленной строки, или явно заблокировав таблицу. Или используйте повторные попытки при ошибке.Я полагаю, что это разработано в соответствии с описанием уровня изоляции для чтения в PostgreSQL 9.2:
Строка вставки в
S1
не существовало еще , когдаS2
«ыDELETE
начали. Так что это не будет видно при удалении вS2
соответствии с ( 1 ) выше. Тот , которыйS1
удален игнорируетсяS2
«s вDELETE
соответствии с ( 2 ).Таким образом
S2
, удаление ничего не делает. Когда вставка идет, хотя, тот действительно видитS1
вставку:Таким образом, попытка вставки
S2
не удалась с нарушением ограничения.Продолжение чтения этого документа, использование повторяемого чтения или даже сериализуемого не решит вашу проблему полностью - второй сеанс завершится ошибкой сериализации при удалении.
Это позволит вам повторить транзакцию.
источник
Я полностью согласен с превосходным ответом @ Mat . Я только пишу другой ответ, потому что он не вписывается в комментарий.
В ответ на ваш комментарий:
DELETE
S2 уже подключен к конкретной версии строки. Поскольку S1 тем временем убивает его, S2 считает себя успешным. Хотя это и не бросается в глаза, серия событий фактически такова:Это все по замыслу. Вам действительно нужно использовать
SERIALIZABLE
транзакции для своих требований и убедиться, что вы повторите попытку при сериализации.источник
Используйте DEFERRABLE первичный ключ и попробуйте снова.
источник
Мы также столкнулись с этой проблемой. Наше решение добавляется
select ... for update
раньшеdelete from ... where
. Уровень изоляции должен быть «Read Committed».источник