Проблема с блокировкой при одновременном удалении / вставке в PostgreSQL

35

Это довольно просто, но я озадачен тем, что делает PG (v9.0). Начнем с простой таблицы:

CREATE TABLE test (id INT PRIMARY KEY);

и несколько строк:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

Используя мой любимый инструмент запросов JDBC (ExecuteQuery), я подключаю два сеансовых окна к БД, где находится эта таблица. Оба они являются транзакционными (то есть, auto-commit = false). Давайте назовем их S1 и S2.

Один и тот же бит кода для каждого:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

Теперь запустите это в замедленном режиме, выполняя по одному в окнах.

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

Теперь это прекрасно работает в SQLServer. Когда S2 выполняет удаление, он сообщает об удалении 1 строки. И тогда вставка S2 работает нормально.

Я подозреваю, что PostgreSQL блокирует индекс в таблице, где существует эта строка, тогда как SQLServer блокирует фактическое значение ключа.

Я прав? Можно ли заставить это работать?

DaveyBob
источник

Ответы:

39

Мэт и Эрвин оба правы, и я только добавляю еще один ответ, чтобы еще больше расширить сказанное, что не вписывается в комментарий. Так как их ответы, кажется, не удовлетворяют всех, и было предложение, чтобы консультации с разработчиками 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.

kgrittn
источник
Интересно, действительно ли дешевый (сырный?) Способ сделать эту работу состоит в том, чтобы выпустить два УДАЛЕНИЯ, за которыми следует ВСТАВКА. В моем ограниченном (2 потока) тестировании это работало нормально, но нужно проверить больше, чтобы проверить, подходит ли это для многих потоков.
DaveyBob
Пока вы используете READ COMMITTEDтранзакции, у вас есть условие гонки: что произойдет, если другая транзакция вставит новую строку после первого DELETEзапуска и до начала второй DELETE? С транзакциями менее строгими, чем SERIALIZABLEдва основных способа закрытия условий гонки, - это продвижение конфликта (но это не помогает, когда строка удаляется) и материализация конфликта. Вы могли материализовать конфликт, имея таблицу «id», которая была обновлена ​​для каждой удаленной строки, или явно заблокировав таблицу. Или используйте повторные попытки при ошибке.
кгрит
Повторите это. Большое спасибо за ценную информацию!
DaveyBob
21

Я полагаю, что это разработано в соответствии с описанием уровня изоляции для чтения в PostgreSQL 9.2:

Команды UPDATE, DELETE, SELECT FOR UPDATE и SELECT FOR SHARE ведут себя так же, как и команды SELECT, с точки зрения поиска целевых строк: они будут находить только те целевые строки, которые были зафиксированы на момент запуска команды 1 . Однако такая целевая строка, возможно, уже была обновлена ​​(или удалена, или заблокирована) другой параллельной транзакцией к тому времени, когда она найдена. В этом случае потенциальный обновитель будет ожидать первой транзакции обновления, чтобы зафиксировать или откатить (если она все еще выполняется). Если первый модуль обновления откатывается назад, то его эффекты отменяются, и второй модуль обновления может продолжить обновление первоначально найденной строки. Если первый обновитель фиксирует, второй обновитель проигнорирует строку, если первый обновитель удалил ее 2в противном случае он попытается применить свою операцию к обновленной версии строки.

Строка вставки в S1не существовало еще , когда S2«ы DELETEначали. Так что это не будет видно при удалении в S2соответствии с ( 1 ) выше. Тот , который S1удален игнорируется S2«s в DELETEсоответствии с ( 2 ).

Таким образом S2, удаление ничего не делает. Когда вставка идет, хотя, тот действительно видит S1вставку:

Поскольку режим Read Committed запускает каждую команду с новым снимком, который включает в себя все транзакции, зафиксированные до этого момента, последующие команды в той же транзакции в любом случае будут видеть результаты совершенной параллельной транзакции . Выше рассмотрен вопрос, видит ли одна команда абсолютно непротиворечивое представление базы данных.

Таким образом, попытка вставки S2не удалась с нарушением ограничения.

Продолжение чтения этого документа, использование повторяемого чтения или даже сериализуемого не решит вашу проблему полностью - второй сеанс завершится ошибкой сериализации при удалении.

Это позволит вам повторить транзакцию.

Мат
источник
Спасибо Мат. Хотя, похоже, именно это и происходит, в этой логике, похоже, есть изъян. Мне кажется, что на уровне iso READ_COMMITTED эти два оператора должны успешно выполняться внутри tx: DELETE FROM test WHERE ID = 1 INSERT INTO test VALUES (1) Я имею в виду, если я удаляю строку и затем вставляю строку, тогда эта вставка должна быть успешной. SQLServer получает это право. На самом деле, мне очень трудно разобраться с этой ситуацией в продукте, который должен работать с обеими базами данных.
DaveyBob
11

Я полностью согласен с превосходным ответом @ Mat . Я только пишу другой ответ, потому что он не вписывается в комментарий.

В ответ на ваш комментарий: DELETES2 уже подключен к конкретной версии строки. Поскольку S1 тем временем убивает его, S2 считает себя успешным. Хотя это и не бросается в глаза, серия событий фактически такова:

   S1 УДАЛИТЬ успешно  
УДАЛЕНИЕ S2 (успешно по доверенности - УДАЛЕНИЕ с S1)  
   S1 повторно вставляет удаленное значение практически в то же время  
S2 INSERT завершается с нарушением ограничения уникального ключа

Это все по замыслу. Вам действительно нужно использовать SERIALIZABLEтранзакции для своих требований и убедиться, что вы повторите попытку при сериализации.

Эрвин Брандштеттер
источник
1

Используйте DEFERRABLE первичный ключ и попробуйте снова.

Фрэнк Хейкенс
источник
спасибо за совет, но использование DEFERRABLE не имело никакого значения. Документ читает так, как должен, но не читает .
DaveyBob
-2

Мы также столкнулись с этой проблемой. Наше решение добавляется select ... for updateраньше delete from ... where. Уровень изоляции должен быть «Read Committed».

Миан Хуан
источник