Одновременные вызовы одной и той же функции: как возникают тупики?

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 :: text, 'hex'))
        Процесс 12379: выберите new_customer (декодировать ($ 1 :: text, 'hex'))
2015-07-28 08:02:58 Подсказка BST: подробности запроса см. В журнале сервера.
2015-07-28 08:02:58 BST КОНТЕКСТ: SQL-оператор "new_customer" оператор 1
2015-07-28 08:02:58 BST STATEMENT: выберите new_customer (декодировать ($ 1 :: text, 'hex'))

связь:

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

Интересно, что это устраняет тупики.

Джек говорит, попробуйте topanswers.xyz
источник
2
В той же транзакции, перед входом в эту функцию, customerиспользуется способ, который бы захватил более слабую блокировку? Тогда это может быть проблемой обновления блокировки.
Даниэль Верите
2
Я не могу это объяснить. Даниэль может иметь точку. Возможно, стоит поднять это на pgsql-general. В любом случае, вам известно о внедрении UPSERT в следующем выпуске Postgres 9.5? Депеш смотрит на это.
Эрвин Брандштеттер,
2
Я имею в виду одну и ту же транзакцию, а не только одну и ту же сессию (поскольку блокировки снимаются в конце передачи). Ответ @alexk - это то, о чем я думал, но если tx начинается и заканчивается функцией, это не может объяснить тупик.
Даниэль Верите
1
@ Эрвин, тебе, несомненно, будет интересен ответ, который я получил от публикации в pgsql-bugs :)
Джек говорит, попробуй topanswers.xyz
2
Очень интересно на самом деле. Имеет смысл, что это работает и в plpgsql, так как я помню подобные случаи plpgsql, работающие как ожидалось.
Эрвин Брандштеттер,

Ответы:

10

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

Я считаю, что проблема заключается в том, что функция SQL будет выполнять синтаксический анализ (и, возможно, планирование тоже; не хочется проверять код прямо сейчас) сразу для всего тела функции. Это означает, что благодаря команде INSERT вы получаете RowExclusiveLock для таблицы «test» во время синтаксического анализа тела функции до фактического выполнения команды LOCK. Таким образом, LOCK представляет попытку эскалации блокировки, и следует ожидать взаимных блокировок.

Этот метод кодирования был бы безопасен в plpgsql, но не в функции языка SQL.

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

С уважением, Том Лейн

Это также объясняет, почему блокировка таблицы за пределами функции в оберточном блоке plpgsql (как предлагает @ypercube) предотвращает взаимные блокировки.

Джек говорит, попробуйте topanswers.xyz
источник
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блокировку. В этот момент он должен ждать 1-го сеанса, поскольку EXCLUSIVEблокировка конфликтует с ROW EXCLUSIVE. Наконец, 1-й сеанс перепрыгивает акул и пытается получить EXCLUSIVEблокировку, но поскольку блокировки получаются по порядку, он ставится в очередь после 2-го сеанса. Это, в свою очередь, ждет 1-го, что приводит к тупику:

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 / почему это так сложно / ).

AlexK
источник
2
Это все интересно, но сообщение в журналах БД будет выглядеть примерно так: 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'))- это указывает на то, что вызов функции является первой командой в обеих транзакциях (если я что-то не упустил).
Эрвин Брандштеттер,
Спасибо, и я согласен с тем, что вы говорите, но это не является причиной в этом случае. Это более понятно в более минимальном тестовом примере, который я добавил к вопросу (который вы можете попробовать сами).
Джек говорит, попробуйте topanswers.xyz
2
На самом деле выясняется, что вы были правы в отношении увеличения блокировки - хотя механизм и тонкий .
Джек говорит, что попробуйте topanswers.xyz