Проблема з блокуванням паралельних DELETE / INSERT в PostgreSQL


35

Це досить просто, але мене бентежить те, що робить PG (v9.0). Почнемо з простої таблиці:

CREATE TABLE test (id INT PRIMARY KEY);

і кілька рядів:

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

Використовуючи мій улюблений інструмент запитів JDBC (ExecuteQuery), я підключаю два вікна сеансу до db, де живе ця таблиця. Обидва вони є транзакційними (тобто, автоматична фіксація = хибна). Назвемо їх 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 блокує фактичне значення ключа.

Чи правий я? Чи можна це змусити працювати?

Відповіді:


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), яка має як блокування, так і помилки серіалізації (у вигляді тупиків).

Повне розкриття: я працював з Dan Ports з MIT, щоб додати справді серіалізаційні транзакції до PostgreSQL версії 9.1, використовуючи нову методику, що називається Serializable Snapshot Isolation.


Мені цікаво, чи дійсно дешевий (сирний?) Спосіб зробити цю роботу - це випустити два ВИДАЛЕННЯ, а потім ВСТУП. У моєму обмеженому (2-х потокових) тестуваннях воно працювало нормально, але потрібно перевірити більше, щоб побачити, чи це буде утримуватися для багатьох потоків.
DaveyBob

Поки ви використовуєте READ COMMITTEDтранзакції, у вас є умова перегонів: що буде, якщо інша транзакція вставила новий рядок після першого DELETEі перед початком другого DELETE? Якщо транзакції менш суворі, ніж SERIALIZABLEдва основні способи закрити умови змагань - це просування конфлікту (але це не допомагає при видаленні рядка) та матеріалізація конфлікту. Ви можете матеріалізувати конфлікт, маючи таблицю "id", яка оновлювалася для кожного видаленого рядка, або явно блокуючи таблицю. Або використовувати повторні помилки.
kgrittn

Повторює це. Велике спасибі за цінне розуміння!
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вставку «s:

Оскільки режим «Читання зобов’язаних» починає кожну команду з нового знімка, який включає всі транзакції, здійснені до цього моменту, наступні команди в тій же транзакції побачать наслідки вчиненої одночасної транзакції в будь-якому випадку . Питання, про яке йде мова, полягає в тому, чи бачить одна чи інша команда абсолютно послідовний вигляд бази даних.

Тож спроба вставки S2провалилася з порушенням обмеження.

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

Це дозволить вам спробувати операцію.


Спасибі Мат. Хоча це, здається, і відбувається, але в цій логіці, здається, є недолік. Мені здається, що на рівні READ_COMMITTED iso, ці два висловлювання повинні досягти успіху всередині tx: ВИДАЛИТИ ІЗ тесту, Де ID = 1 ВСТАВИТЬСЯ В ТЕСТИ ЗНАЧЕННЯ (1) Я маю на увазі, якщо я видаляю рядок, а потім вставляю рядок, тоді ця вставка повинна досягти успіху. SQLServer отримує це право. Насправді мені дуже важко займатися цією ситуацією в продукті, який повинен працювати з обома базами даних.
DaveyBob

11

Я повністю згоден з відмінною відповіддю @ Mat . Я пишу лише іншу відповідь, тому що вона не впишеться в коментар.

У відповідь на ваш коментар: DELETEу S2 вже зачеплений певний рядовий варіант. Оскільки цього часу S1 вбиває, S2 вважає себе успішним. Хоча це очевидно не з швидкого погляду, низка подій практично така:

   S1 DELETE вдалий  
S2 DELETE (успішно проксі - DELETE від S1)  
   S1 повторно ВСТАВЛЯЄТЬСЯ вилучене значення практично тим часом  
S2 INSERT виходить з ладу з унікальним порушенням обмеження ключа

Це все за дизайном. Вам дійсно потрібно використовувати SERIALIZABLEтранзакції для своїх вимог і переконайтеся, що ви повторите спробу при збої серіалізації.


1

Скористайтеся первинним ключем DEFERRABLE та повторіть спробу.


дякую за пораду, але використання DEFERRABLE взагалі не змінилося. Документ читає, як має бути, але ні.
DaveyBob

-2

Ми також зіткнулися з цим питанням. Наше рішення додається select ... for updateраніше delete from ... where. Рівень ізоляції повинен бути зчитований.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.