そのデッドロック、知ってました?

このエントリーをはてなブックマークに追加
こんにちわ。開発エンジニアのsugimotoです。
今回はPostgresqlのデッドロックについてちょっと書いてみます。
確認した環境はPostgresql9.1.4です。
ロックとかデッドロックについての説明は省きます。



デッドロックの調査

カタログテーブルのpg_locksを確認すると現在のロック状態がわかります。
pg_classと結合すればテーブル名もわかります。
各ロックの競合関係図はhttp://www.ecoop.net/memo/archives/2008-05-30-1.htmlがわかりやすくてお勧めです。



デッドロックの例

自分が経験したことのあるデッドロックの例を3つほどあげます。

例1.明示的なロックによるデッドロック

一番普通のパターンです。
本にはこれが載っていることが多いです。
自分もつい2年くらい前まではデッドロックはこのパターンしかないと思ってました。



上の図のように複数のトランザクションが2つのリソースに対し互い違いにロックを掛けようとして起きます。


例2.selectとalterによるデッドロック

予想外のパターンその1です。
selectしてるだけなのに何故デッドロックするの???と最初わけがわからなかったです。



selectがロックモードのAccessShareLock、alterがAccessExclusiveLockを獲得するために、互い違いの実行になると競合するわけです。
selectの実行で何故ロックを掛けるの?って思いましたが、トランザクション内で列が減ったりすると整合性が保てなくなるためにしょうがないです。
alter以外にもselectと競合するコマンドはあります。
http://www.postgresql.jp/document/9.1/html/explicit-locking.html#LOCKING-TABLESを参照してください。


例3.参照制約によるデッドロック

予想外のパターンその2です。
これも知らなくて結構はまりました。




他のテーブルへの参照制約があるテーブルに対してinsert、update、deleteを行うと、RowExclusiveLockを獲得します。
どうもそれだけではないらしく、pg_locksを確認するとinsert時には参照先のテーブルに対してはRowShareLockを獲得するようです(たしかに参照先のデータがいきなり消えたりしたら困ります)。
そういうわけで互い違いにロックを掛けてデッドロックが起こるんだなっと思ってました。
しかし、どうやらことはそんな単純ではないらしい。
ロックモードの関係図を見るとRowExclusiveLockとRowShareLockは競合しない。
ということはこれらだけだとデッドロックが起こらない。
ということでこの辺をもう少し調べてみました。



参照制約のロック調査

まずは調査のためにテーブルを作成し、初期データを入れます。
create table t1(i integer primary key, j integer);
create table t2(i integer primary key, j integer references t1(i));
insert into t1 values(1, 1);
insert into t1 values(2, 2);


t2のカラムjがt1のカラムiを参照しています。
この時点で次のコマンドでロックを見てもありません。

select l.locktype, l.pid, l.page, l.tuple, c.relname, l.mode from pg_locks as l, pg_class as c where l.relation=c.oid and c.relname not like 'pg_%';(他にユーザのロックが出ないようにデータベースで絞った方が良い)
準備が出来たので、参照制約によるデッドロックを発生させ、その時点のロック状況を確認します。
デッドロックは次の手順で発生させます。


1.トランザクションAを開始し(pid=4292)、t1のi=1のデータを更新
2.トランザクションBを開始し(pid=5504)、t2にt1のi=2のデータを参照するデータを登録
3.トランザクションAにおいて、t1のi=2のデータを更新
4.トランザクションBにおいて、t2にt1のi=1のデータを参照するデータを登録

postgresqlがデッドロックを検知し処理を停止する前にロック状況を確認すると以下のようになっています。



locktype | page | tuple | pid  | relname |       mode       
----------+------+-------+------+---------+------------------
relation |      |       | 4292 | t1_pkey | RowExclusiveLock
relation |      |       | 5504 | t2      | RowExclusiveLock
relation |      |       | 5504 | t1      | RowShareLock     
relation |      |       | 4292 | t1      | RowExclusiveLock
tuple    |    0 |     3 | 4292 | t1      | ExclusiveLock    
relation |      |       | 5504 | t2_pkey | RowExclusiveLock
tuple    |    0 |     1 | 5504 | t1      | ShareLock   


予想していたRowExclusiveLock、RowShareLock以外にも出ています。行ロックですね。
posgresqlのマニュアルにはpg_locksには行ロックは表示されないとあったのですが、ロック解放待ちのロックは表示されるようです。
恐らくtuple=3がt1のi=2のデータであり、tuple=1がt1のi=1のデータのことだと思います。
まずトランザクションAですが、i=2への更新時に取得する行ロックのExclusiveLockを取得できずに待っています。
これはトランザクションBが獲得したRowShareLockもしくはRowShareLock時に獲得したと思われる行ロックのShareLockに阻まれているからだと思います。
次にトランザクションBですが、i=1へのRowShareLock獲得のために行ロックのShareLockを取得できずに待ってます。
これはトランザクションAがi=1の更新時に獲得したExclusiveLockに阻まれているからだと思います。

ということでテーブルロックだけでは分かりませんでしたが、行ロックまで考えるとなんとなく分かったのでこれでOKとします。
行ロックがどこかで見れたら良いのですが、調べた限りだとどこにもでないようです。

次の記事
« Prev Post
前の記事
Next Post »

2 コメント

Write コメント
Yuya Miyazaki
AUTHOR
2017年11月27日 18:15 delete

参考になる記事をありがとうございます。

今後検索で訪れる方のための補足なのですが、例3は最新のPostgresでは再現しません。
おそらく外部キー制約による参照先テーブルの行ロックが、9.3で導入されたFOR KEY SHAREを使うようになり、競合しにくくなったためです。
http://paquier.xyz/postgresql-2/postgres-9-3-feature-highlight-for-key-share-and-for-no-key-update/

そもそも外部キー制約はキーの参照先の行の存在だけを制約するもので、行の存在や特定に必要となるキーに関係ない更新までブロックしてしまう必要はないので、より細かいレベルでロックを使い分けられるようにしたということのようです。
とはいえ安易にSELECT FOR UPDATEを使うとやはりデッドロックが発生することはあるため、被参照列の更新やDELETEが発生しない場合はSELECT FOR KEY UPDATEするといった対策を意識しておく必要がありそうです。

Reply
avatar
ishikawa
AUTHOR
2017年11月28日 17:02 delete

ありがとうございます。sugimotoに代わり御礼申し上げます。

Reply
avatar
Related Posts Plugin for WordPress, Blogger...