TT Lab
はじめる
学ぶ 学習パス コース

並行性 — 二人が同じ行を触るとき

二つのセッションでぶつけてみる

TT Labで続きを見る

目標

1人で動かすと正しいのに、2人が同時にやると間違うコードを、自分で作って直します。 並行性の問題は再現が難しいから難しいのです。そのため、このラボはすべてセッションを2つ 実際に立ち上げてぶつけます。

環境

PostgreSQL 16がすでに起動しています(5432、DB labdb、ユーザーlab)。

export PATH=/usr/lib/postgresql/16/bin:$PATH
psql -h 127.0.0.1 -U lab -d labdb

2つのセッションの作り方

片方をバックグラウンドで起動し、その間にもう片方を操作します。

psql -h 127.0.0.1 -U lab -d labdb   -c "begin; select * from stock where id=1 for update; select pg_sleep(15);" &
sleep 1
# 여기서 두 번째 세션 작업
wait

準備

create table stock(id int primary key, qty int not null);
insert into stock values (1,10),(2,10);

ステップ

  1. ロック待ちを作る → 01-block.txt
  2. lock_timeout → 02-timeout.txt
  3. わざとデッドロックを起こす → 03-deadlock.txt
  4. 順序をそろえてなくす → 04-order.txt
  5. 同時に20回減算しても、マイナスにならないようにする → 05-stock.txt
  6. skip lockedでワーカーキュー → 06-queue.txt
  7. アドバイザリーロック → 07-advisory.txt
  8. まとめ → 08-notes.md

参考

ステップ3は失敗が正解です。deadlock detectedのDETAIL 2行が、 誰が何を待ったのかを正確に教えてくれます。その2行の読み方が、 このラボでいちばん長く使える技術です。

ロック待ちを作る

セッション2つで、同じ行をfor updateしてください。2つ目が待っている間に、誰が誰を止めているのかを調べて01-block.txtに残します。

まずcreate table stock(id int primary key, qty int not null); insert into stock values (1,10),(2,10);。片方はバックグラウンドで: psql -h 127.0.0.1 -U lab -d labdb -c "begin; select * from stock where id=1 for update; select pg_sleep(15);" &。そのあと別のセッションでselect pid, pg_blocking_pids(pid) from pg_stat_activity where cardinality(pg_blocking_pids(pid))>0;を保存してください。

無限に待たないようにする

lock_timeoutを設定し、ロックされた行を要求して、キャンセルされることを確認してください。そのエラーの全文を02-timeout.txtに残します。

set lock_timeout='1s'; begin; select * from stock where id=1 for update;でcanceling statement due to lock timeoutが出ます。これをかけないと、待ちがコネクションを握り、そのコネクションがプールを食い尽くすと、DBではなくアプリケーションが止まります。

デッドロックをわざと作る

2つのトランザクションが互いに逆の順序でid 1・2を更新するようにして、デッドロックを起こしてください。deadlock detectedを含むエラーの全文を03-deadlock.txtに残します。

Aは1 → (少し休んで) → 2、Bは2 → (少し休んで) → 1。select pg_sleep(2)を間に入れて重ねてください。2つをバックグラウンドで同時に起動してwaitします。DETAILの2行が、誰が何を待ったのかを正確に教えてくれます。 その2行の読み方が、このステップの目的です。

順序をそろえてなくす

同じ2つの作業が、どちらもid昇順でロックするように変えて、デッドロックなしで終わるようにしてください。2回以上実行しても失敗がない必要があります。

並べ替えの基準は何でもよく、全員が同じ基準を使うことだけが重要です。結果を04-order.txtに残してください。デッドロックは完全にはなくせないので、アプリケーションは40P01のリトライを備える必要がありますが、順序をそろえればほとんどは消えます。

在庫がマイナスにならないように

stockのid=1を10に戻したあと、同時に20回の減算を試みてください。在庫10に対して20回なら、最終的な数量はちょうど0でなければなりません。結果を05-stock.txtに残します。

2つのうち1つで十分です。for updateで読んで判断するか、1つの文で終わらせる(update stock set qty=qty-1 where id=1 and qty>0)かです。可能なら後者が安上がりです。同時実行はfor i in $(seq 20); do (psql ... &) ; done; wait。

マイナスにならないだけでは合格しません。 素朴にselectで読んでその値を引くと、マイナスにはなりませんが、結果が9あたりで止まります。更新が消えたのです。それがこのステップで捕まえようとしているバグです。

ワーカーが列に並ばないように

jobsテーブルを作り、ワーカー2つが互いに違う仕事を取っていくようにしてください。06-queue.txtにworker1=...とworker2=...の2行で、それぞれが取ったidを残します。

for updateだけを使うと、2つ目のワーカーが待ちます。for update skip lockedは、ロックされた行を飛ばします。 最初のワーカーが1、2を握っていれば、2つ目は待たずに3、4を取ります。ファイルはこの形式で。それぞれ1行ずつ、使ったSQLも一緒に残してください:

worker1=1,2
worker2=3,4

行がないものにロックをかける

pg_try_advisory_lockで、同じキーを2つのセッションが同時には取れないことを示してください。07-advisory.txtにsession1=とsession2=の2行で結果を残します。

1つのセッションがselect pg_try_advisory_lock(42)で取得したあと、別のセッションで同じキーを試すとfが出ます(同じセッションでもう一度やると再入なのでtになります。それでは証明になりません)。ファイルはこの形式で:

session1=t
session2=f

コネクションプールでは、返却の前に必ずpg_advisory_unlockしてください。

3つをまとめる

08-notes.mdに3行以上。デッドロックをなくす規則を1つ、lock_timeoutがないとどこが先に止まるか、skip lockedが変えるもの。

本文に순서・커넥션・skip lockedが含まれている必要があります(韓国語の2語は、順に「順序」と「コネクション」を意味します)。2つ目が、実務でいちばんよく原因を取り違える部分です。症状は「DBが遅い」ではなく「サーバーが応答しない」です。