同じ関数の同時呼び出し:デッドロックはどのように発生しますか?


15

私の関数new_customerは、Webアプリケーションによって1秒あたり数回(ただし、セッションごとに1回)呼び出されます。最初に行うことは、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は、データベース12141のリレーション16438でExclusiveLockを待機します。プロセス12379によってブロックされました。
        プロセス12379は、データベース12141の関係16438でExclusiveLockを待ちます。プロセス12380によってブロックされました。
        プロセス12380:new_customer(decode($ 1 :: text、 'hex'))を選択します
        プロセス12379:new_customer(decode($ 1 :: text、 'hex'))を選択
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 STATEMENT:select new_customer(decode($ 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スクリプトは2つの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-generalでこれを上げる価値があるかもしれません。いずれにせよ、今後のPostgres 9.5でのUPSERTの実装を知っていますか?デペスがそれを見て。
アーウィンブランドステッター

2
つまり、同じセッション内ではなく、同じトランザクション内で(ロックはtx終了時に解放されるため)を意味します。@alexkによる答えは、私が考えていたものですが、txが関数で開始および終了する場合、デッドロックを説明できません。
ダニエルベリテ

1
@Erwinは、間違いなくpgsql-bugsでの投稿から得た答えに興味があるでしょう:)
Jack Douglas

2
とても興味深い。同様のplpgsqlのケースが予想どおりに動作することを覚えているので、これはplpgsqlでも機能することは理にかなっています。
アーウィンブランドステッター

回答:


10

私は、この投稿をpgsqlの-バグにし、そこに返信トム・レーンからは、これが処理される方法SQL言語関数の力学によって装ったロック・エスカレーションの問題、であることを示します。基本的に、、によって生成されたロックは、テーブルの排他ロックのinsert取得されます

これに関する問題は、SQL関数が関数本体全体を一度に解析する(そしておそらく計画も立てる、今すぐコードをチェックする気にならない)ことだと思います。これは、INSERTコマンドのために、LOCKコマンドが実際に実行される前に、関数本体の解析中に「テスト」テーブルでRowExclusiveLockを取得することを意味します。したがって、LOCKはロックエスカレーションの試みを表し、デッドロックが予想されます。

このコーディング手法はplpgsqlでは安全ですが、SQL言語関数では安全ではありません。

構文解析が一度に1つのステートメントで発生するようにSQL言語関数を再実装することについての議論がありましたが、その方向で起こっていることについて息を止めないでください。誰にとっても優先度の高い問題ではないようです。

よろしく、トムレーン

これはまた、ラッピングplpgsqlブロックで関数の外でテーブルをロックする理由を説明します( @ypercubeが示唆する)でことでデッドロックが防止される


3
ファインポイント:ypercubeは、実際にプレーンSQLでのロックをテストした明示的なトランザクションに外にある機能、ないと同じplpgsqlがブロック。
アーウィンブランドステッター

1
まったく正しい、私の悪い。私たちが試した別のことと混同していたと思います(デッドロックを防ぐことはできませんでした)。
ジャックダグラス

4

new_customerを呼び出す前に別のステートメントを実行し、それらが競合するロックを取得すると仮定します。 EXCLUSIVE(基本的にはcustomerテーブルのデータ変更)、説明は非常に簡単です。

簡単な例で問題を再現できます(関数を含めないこともできます):

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-so-complicated /)。


2
これはすべて興味深いですが、dbログのメッセージは次のようになります。Jack 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'))-関数呼び出しが両方のトランザクションの最初のコマンドであることを示します(何かが欠落していない限り)。
アーウィンブランドステッター

ありがとう、私はあなたの言うことに同意しますが、これはこの場合の原因ではないようです。これは、質問に追加した最小限のテストケースでは明らかです(自分で試すことができます)。
ジャックダグラス

2
実際には、ロックのエスカレーションについては正しかったことがわかります- メカニズムは微妙ですが。
ジャックダグラス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.