簡単なストアド プロシージャを作成しました。
CREATE OR REPLACE FUNCTION "public"."update_table01" (
int4, -- $1 (population)
int2 -- $2 (id)
) RETURNS "pg_catalog"."void" AS
$body$
BEGIN
UPDATE "table01"
SET
population = $1
WHERE id = $2;
END;
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;
Java サーバー (jre7) 内でこれを呼び出しており、C3P0 を接続プールとして Hibernate 4 を使用しています。これは PostgreSQL 9.2.4 で実行されています。table01 (注釈によってマッピング) に対応する Hibernate エンティティがあり、この手順を SQL 更新として使用するように指定しました。
@SQLUpdate(sql = "{call update_table01(?, ?)}", callable = true)
これを頻繁に呼び出す複数のスレッド (約 20 ~ 30) で負荷テストを行ったところ、驚いたことに、いくつかのデッドロックが発生しました。ログの関連部分は次のとおりです。
2013-08-14 14:51:19 CEST [23495]: [3-1] LOG: process 23495 detected deadlock while waiting for ShareLock on transaction 140127434 after 1000.048 ms
2013-08-14 14:51:19 CEST [23495]: [4-1] STATEMENT: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23495]: [5-1] ERROR: deadlock detected
2013-08-14 14:51:19 CEST [23495]: [6-1] DETAIL: Process 23495 waits for ShareLock on transaction 140127434; blocked by process 23481.
Process 23481 waits for ShareLock on transaction 140127431; blocked by process 23495.
Process 23495: update table01 set population=$1 where id=$2
Process 23481: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23495]: [7-1] HINT: See server log for query details.
2013-08-14 14:51:19 CEST [23495]: [8-1] STATEMENT: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23481]: [3-1] LOG: process 23481 still waiting for ShareLock on transaction 140127431 after 1000.086 ms
2013-08-14 14:51:19 CEST [23481]: [4-1] STATEMENT: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23481]: [5-1] LOG: process 23481 acquired ShareLock on transaction 140127431 after 1000.227 ms
2013-08-14 14:51:19 CEST [23481]: [6-1] STATEMENT: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23938]: [3-1] LOG: process 23938 still waiting for ExclusiveLock on tuple (8,72) of relation 16890 of database 16751 after 1000.119 ms
2013-08-14 14:51:19 CEST [23938]: [4-1] STATEMENT: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23938]: [5-1] LOG: process 23938 acquired ExclusiveLock on tuple (8,72) of relation 16890 of database 16751 after 1000.174 ms
2013-08-14 14:51:19 CEST [23938]: [6-1] STATEMENT: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23520]: [3-1] LOG: duration: 970.319 ms execute <unnamed>: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23520]: [4-1] DETAIL: parameters: $1 = '5731', $2 = '294'
2013-08-14 14:51:19 CEST [23481]: [7-1] LOG: duration: 1000.361 ms execute <unnamed>: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23481]: [8-1] DETAIL: parameters: $1 = '1586', $2 = '253'
2013-08-14 14:51:19 CEST [23524]: [3-1] LOG: duration: 531.909 ms execute <unnamed>: update table01 set population=$1 where id=$2
2013-08-14 14:51:19 CEST [23524]: [4-1] DETAIL: parameters: $1 = '1546', $2 = '248'
2013-08-14 14:51:19 CEST [23938]: [7-1] LOG: duration: 1004.863 ms execute <unnamed>: update table01 set population=$1 where id=$2
私は Postgres のログを解釈するのが苦手なので、間違っていたら訂正してください。これは、2 つのプロセスがデッドロックに陥ったことを示していると思います。どちらも同じステートメントを実行していました。
多くのことが彼女を困惑させます。最も重要なことは、ストアド プロシージャで特定の ID を持つ行の行レベル ロックが 1 つしか取得されない場合に、2 つのプロセス (異なる行を更新するプロセスであっても) がデッドロックになる可能性があることを理解していないことです。これは、このテーブルを変更する唯一の手順です。排他ロック(SELECT ... FOR UPDATEを実行する場合も同様)を取得することになっていませんか?ShareLocks はどこから来たのですか? 後の行 (23938) に示されているプロセスは何ですか? 私の推測では、23938 もロックを待っていて、23495 が強制終了されたときにロックを取得したということです。
次に、次のことを試しました。
CREATE OR REPLACE FUNCTION "public"."update_table01" (
int4, -- $1 (population)
int2 -- $2 (id)
) RETURNS "pg_catalog"."void" AS
$body$
BEGIN
PERFORM 1 FROM "table01" WHERE id = $2 FOR UPDATE;
UPDATE "table01"
SET
population = $1
WHERE id = $2;
END;
$body$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;
再度実行したところ、再現できませんでした。行き詰まりはなくなりました。
なぜこうなった?
編集:少し調査した後、セッションをフラッシュするときに、Hibernate がこのメソッドを単独で呼び出しているようです。場合によっては、異なるエンティティに対して、すべて同じトランザクションで数回。update_table01() を呼び出すたびに特定の table01 列の行が FOR UPDATE ロックでロックされるため、デッドロックが発生する可能性があります。呼び出し順序が不適切な場合、循環待機が発生する可能性があります。このエンティティを更新不可にした後 (つまり、すべての列に update=false をマークした後)、すべてが正常に機能します。RAM 内の table01 エンティティは、後のトランザクションを担当するセッションのいずれにも接続されていなかったため、この Hibernate の動作には本当に驚きました。それでも、Hibernate はこれらのエンティティをデータベースにフラッシュしましたが、その理由はわかりません。
ロックに関しては、table01 を参照する他のテーブルに挿入/更新する 2 つのストアド プロシージャを特定しました (そのうちの 1 つは FK 列を変更します)。これらは、table01 の適切な FK 行で ShareLock を要求するため、update_table01() と競合します。したがって、これら 3 つのストアド プロシージャは、相互に完了するまで待機します。これだけでは循環待機を作成することはできませんが、これらのストアド プロシージャの呼び出しの後に、Hibernate によって引き起こされる update_table01() への呼び出しをいくつか追加すると可能になります。