弁財天

ゴフマン「専門家を信じるのではなく、自分自身で考えて判断せよ」

PostgreSQLの排他制御

12.2.1. Read Committed Isolation Level

The search condition of the command (the WHERE clause) is re-evaluated to see if the updated version of the row still matches the search condition. If so, the second updater proceeds with its operation, starting from the updated version of the row. (In the case of SELECT FOR UPDATE and SELECT FOR SHARE, that means it is the updated version of the row that is locked and returned to the client.)

SELECT FOR UPDATEはロックではなく、MVCCで更新行の再評価のために使う
SELECT FOR UPDATEを使うと単発のSQLは再計算(re-evaluated)される

コアチーム のTom Laneによる同時実行性のプレゼン資料

http://www.postgresql.org/
Developpers
→coding のリンク
→→Presentations
→→→PostgreSQL Concurrency Issues (From OSCON 2002)

BEGIN;
SELECT SUM(残高) FROM 口座;
SELECT SUM(支店残高) FROM 支店;
-- 同じ結果が得られることを確認する
COMMIT;
このSQLは分離レベルの設定で動作が異なる
SERIALIZABLEに設定では動作する
READ COMMITTEDではうまく動作しない

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SERIALIZABLEに設定することで
トランザクションの開始からコミットまで一貫性を持たせることが可能
READ COMMITTEDではSQL発行の度の一貫性しかない
SERIALIZABLEに設定することで、排他制御が成立する
分離レベルをSERIALIZABLEに設定、
"ERROR: could not serialize access due to concurrent update"
PostgreSQLは競合したトランザクションをブロックしないでエラーリターンさせる

エラーを利用したトライアンドエラー

トム・レーンのリトライループ
Solution #2: use SERIALIZABLE mode and retry on serialization error

loop
	BEGIN;
	SELECT hits FROM webpages WHERE url = ’...’;
	-- client internally computes $newval = $hits + 1
	UPDATE webpages SET hits = $newval WHERE url = ’...’;
	if (no error)
		break out of loop;
	else
		ROLLBACK;
	end loop
	COMMIT;
エラー発生時にロールバックして、hits列を再読み込みするので
更新が競合しても問題なく動作する

BEGINがjdbcでも機能するか?

PostgreSQLのデフォルトはREAD COMMITTED
排他制御を考慮してないフレームワークでは
同じ行への更新が競合したときに、データの消失が発生しているだろう
デッドロックは、その警告でもあった

UPDATE webpages SET hits = hits + 1 WHERE url = ’...’;
CLIENT 1CLIENT 2
reads row, sees hits = 531 
 reads row, sees hits = 531
computes hits+1 = 532 
 computes hits+1 = 532
writes hits = 532 
 writes hits = 532
commit 
 commit
hitsが533になることを期待したロジックは、
結局は532になって誤動作

READ COMMITTEDモードでは「単発の」SQLは再評価されるので問題なく動作する。

CLIENT 1CLIENT 2
reads row, sees hits = 531 
 reads row, sees hits = 531
computes hits+1 = 532 
 computes hits+1 = 532
writes hits = 532 
 tries to write row, must wait
commit 
 re-reads row, sees hits = 532
re-computes hits+1 = 533
writes hits = 533
commit
the second client is forced to redo his computation:
2つめのクライアントは「強制的に再計算が行われる」
単発のUPDATE文でインクリメントすると、強制的な再計算が行われて辻褄があう

しかし、複数のSQLで処理を行う場合、READ COMMITTEDの再計算機能は役に立たない

BEGIN;
SELECT hits FROM webpages WHERE url = ’...’;
-- client internally computes $newval = $hits + 1
UPDATE webpages SET hits = $newval WHERE url = ’...’;
COMMIT;

ここでSELECT FOR UPDATE登場

BEGIN;
SELECT hits FROM webpages WHERE url = ’...’ FOR UPDATE;
-- client internally computes $newval = $hits + 1
UPDATE webpages SET hits = $newval WHERE url = ’...’;
COMMIT;
SELECT FOR UPDATEをかけたときの動作
CLIENT 1CLIENT 2
locks row, reads hits = 531 
 tries to lock row, must wait
マニュアルに記述されてないU-Lock
それも行ロックで登場、話が違う
computes $newval = 532
writes hits = 532
commit
 
 locks row, reads hits = 532
computes $newval = 533
writes hits = 533
commit
BEGIN/COMMITの中でSELECT FOR UPDATEを使うと排他制御を行ってくれる

この2002年のドキュメントのPostgreSQLのバージョンは不明

競合が多いときは、READ COMMITTED と SELECT FOR UPDATE の組み合わせ
少ないときは、SERIALIZABLE と リトライループの組み合わせる

この会社のフレームワークは排他制御をかけた更新ができない
pgplsqlも導入されてなく、ストアドプロシージャも使えなかった
ラッパーが、単発のUPDATEしか発行できない状態にしていた
PostgreSQLに限らず、どんなデータベースと組み合わせても、まともな更新はできていないだろう
アクセスが集中すればする程、更新処理が誤動作する情況

投稿されたコメント:

コメント
コメントは無効になっています。