Одночасні дзвінки на одну і ту ж функцію: як відбуваються тупикові місця?


15

Моя функція new_customerвикликається веб-додатком кілька разів на секунду (але лише один раз за сеанс). Найперше, що він робить - це заблокувати customerтаблицю (зробити "вставку, якщо її немає" - простий варіант а upsert).

Я розумію документи , що інші дзвінки new_customerповинні просто стояти в черзі, поки всі попередні дзвінки не закінчаться:

LOCK TABLE отримує блокування на рівні таблиці, чекаючи, якщо це необхідно, для звільнення будь-яких конфліктних замків.

Чому іноді замість цього відбувається тупикова ситуація?

визначення:

create function new_customer(secret bytea) returns integer language sql 
                security definer set search_path = postgres,pg_temp as $$
  lock customer in exclusive mode;
  --
  with w as ( insert into customer(customer_secret,customer_read_secret)
              select secret,decode(md5(encode(secret, 'hex')),'hex') 
              where not exists(select * from customer where customer_secret=secret)
              returning customer_id )
  insert into collection(customer_id) select customer_id from w;
  --
  select customer_id from customer where customer_secret=secret;
$$;

помилка з журналу:

2015-07-28 08:02:58 BST ДЕТАЛІ: Процес 12380 очікує на ExclusiveLock щодо відношення 16438 бази даних 12141; заблокований процесом 12379.
        Процес 12379 очікує на ExclusiveLock щодо відношення 16438 бази даних 12141; заблокований процесом 12380.
        Процес 12380: виберіть new_customer (декодування ($ 1 :: текст, «шістнадцятковий»))
        Процес 12379: виберіть new_customer (декодування ($ 1 :: текст, "шістнадцятковий"))
2015-07-28 08:02:58 BST Підказка: Перегляньте журнал сервера, щоб отримати детальну інформацію про запити.
2015-07-28 08:02:58 BST CONTEXT: SQL-функція "new_customer" 1
2015-07-28 08:02:58 BST ЗАЯВКА: виберіть new_customer (декодування ($ 1 :: текст, "шістнадцятковий"))

відношення:

postgres=# select relname from pg_class where oid=16438;
┌──────────┐
 relname  
├──────────┤
 customer 
└──────────┘

редагувати:

Мені вдалося отримати простий тестовий випадок, який можна відтворити. Для мене це схоже на помилку через якийсь стан перегонів.

схема:

create table test( id serial primary key, val text );

create function f_test(v text) returns integer language sql security definer set search_path = postgres,pg_temp as $$
  lock test in exclusive mode;
  insert into test(val) select v where not exists(select * from test where val=v);
  select id from test where val=v;
$$;

скрипт bash запускається одночасно у двох сеансах bash:

for i in {1..1000}; do psql postgres postgres -c "select f_test('blah')"; done

журнал помилок (як правило, декілька тупиків за 1000 викликів):

2015-07-28 16:46:19 BST ERROR:  deadlock detected
2015-07-28 16:46:19 BST DETAIL:  Process 9394 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9393.
        Process 9393 waits for ExclusiveLock on relation 65605 of database 12141; blocked by process 9394.
        Process 9394: select f_test('blah')
        Process 9393: select f_test('blah')
2015-07-28 16:46:19 BST HINT:  See server log for query details.
2015-07-28 16:46:19 BST CONTEXT:  SQL function "f_test" statement 1
2015-07-28 16:46:19 BST STATEMENT:  select f_test('blah')

редагувати 2:

@ypercube запропонував варіант із lock tableзовнішньою функцією:

for i in {1..1000}; do psql postgres postgres -c "begin; lock test in exclusive mode; select f_test('blah'); end"; done

що цікаво, це усуває тупики.


2
У тій же транзакції, перш ніж вступити в цю функцію, customerвикористовується таким чином, щоб схопити слабший замок? Тоді це може бути проблема оновлення блокування.
Даніель Верете

2
Я не можу цього пояснити. Даніель може мати крапку. Можливо, варто підняти це на загальному pgsql. У будь-якому випадку, чи знаєте ви про впровадження UPSERT в майбутньому Postgres 9.5? Депес дивився на це.
Erwin Brandstetter

2
Я маю на увазі в межах однієї транзакції, а не в тому самому сеансі (оскільки блокування випускаються в кінці tx). Відповідь від @alexk - це те, про що я думав, але якщо tx запускається і закінчується функцією, це не може пояснити тупик.
Даніель Веріте

1
@Erwin вас, безсумнівно, зацікавить відповідь, яку я отримав від публікації в pgsql-bugs :)
Джек каже спробувати topanswers.xyz

2
Дійсно дуже цікаво. Має сенс, що це працює і в plpgsql, оскільки я пам’ятаю, що подібні випадки plpgsql працюють як очікувалося.
Erwin Brandstetter

Відповіді:


10

Я опублікував це на pgsql-bugs, і відповідь там від Тома Лейн вказує, що це проблема ескалації блокування, замаскована механікою способу обробки мовних функцій SQL. По суті, замок, що генерується, insertотримується перед ексклюзивним замком на столі :

Я вважаю, що проблема в цьому полягає в тому, що функція SQL буде робити аналіз (і, можливо, планувати; не відчуваю, як перевірити код зараз) для всього органу функції одразу. Це означає, що завдяки команді INSERT ви отримуєте RowExclusiveLock в "тестовій" таблиці під час розбору тіла функції, перш ніж команда LOCK насправді виконує. Таким чином, LOCK являє собою спробу ескалації блокування, і слід очікувати тупикових ситуацій.

Ця методика кодування була б безпечною у plpgsql, але не у функції мови SQL.

Були обговорені питання щодо повторного втілення функцій мови SQL так, що розбір відбувається по одній заяві за часом, але не затримуйте дихання з приводу того, що відбувається в цьому напрямку; це, мабуть, не є важливим для когось пріоритетного.

З повагою, Том Лейн

Це також пояснює, чому блокування таблиці поза функцією в обгортковому блоці plpgsql (як це запропонував @ypercube) запобігає тупиковість.


3
Тонка точка: ypercube фактично перевіряв блокування в простому SQL в явній транзакції поза функцією, яка не є такою ж, як блок plpgsql .
Ервін Брандштеттер

1
Цілком правильно, моя погана. Я думаю, що я плутався з іншою справою, яку ми спробували (яка не завадила тупику).
Джек каже, спробуйте topanswers.xyz

4

Якщо припустити, що ви запускаєте ще одні заяви перед тим, як викликати new_customer, і ті набувають блокування, яке суперечить EXCLUSIVE(в основному, будь-яка зміна даних у таблиці клієнтів), пояснення дуже просте.

Можна відтворити проблему простим прикладом (навіть не включаючи функцію):

CREATE TABLE test(id INTEGER);

1-й сеанс:

BEGIN;

INSERT INTO test VALUES(1);

2-й сеанс

BEGIN;
INSERT INTO test VALUES(1);
LOCK TABLE test IN EXCLUSIVE MODE;

1-й сеанс

LOCK TABLE test IN EXCLUSIVE MODE;

Коли перший сеанс робить вставку, він набуває ROW EXCLUSIVEблокування на столі. Тим часом, сесія 2 намагається також отримати ROW EXCLUSIVEблокування та намагається придбати EXCLUSIVEблокування. Після цього доводиться чекати першого сеансу, оскільки EXCLUSIVEблокування конфліктує з ROW EXCLUSIVE. Нарешті, 1-й сеанс стрибає з акул і намагається дістати EXCLUSIVEзамок, але оскільки блокування придбано в порядку, він виходить у черги після 2-го сеансу. Це, у свою чергу, чекає першого, створюючи тупик:

DETAIL:  Process 28514 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28084.
Process 28084 waits for ExclusiveLock on relation 58331454 of database 44697822; blocked by process 28514

Вирішення цієї проблеми полягає в придбанні замків якомога раніше, як правило, в першу чергу в угоді. З іншого боку, завантаження PostgreSQL потребує блокування лише в деяких дуже рідкісних випадках, тому я б запропонував переосмислити те, як ви робите прихистку (подивіться цю статтю http://www.depesz.com/2012/06/10 / why-is-upsert - так складне / ).


2
Це все цікаво, але повідомлення в журналах db писало б щось на кшталт: Process 28514 : select new_customer(decode($1::text, 'hex')); Process 28084 : BEGIN; INSERT INTO test VALUES(1); select new_customer(decode($1::text, 'hex'))У той час як Джек щойно отримав: Process 12380: select new_customer(decode($1::text, 'hex')) Process 12379: select new_customer(decode($1::text, 'hex'))- вказуючи на те, що виклик функції - це перша команда в обох транзакціях (якщо я щось не пропускаю).
Erwin Brandstetter

Дякую, і я згоден з тим, що ви говорите, але це, мабуть, не є причиною в цьому випадку. Це зрозуміліше в більш мінімальному тестовому випадку, який я додав до питання (яке ви можете спробувати самостійно).
Джек каже, спробуйте topanswers.xyz

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