サーバーのステータス(「アクティブ」、「スタンバイ」など)などのサーバーのクラスターに関する詳細を含むPostgresデータベースがあります。アクティブなサーバーはいつでもスタンバイにフェイルオーバーする必要がある場合があり、特にどのスタンバイが使用されているかは気にしません。
データベースクエリでスタンバイのステータスを変更し(1つだけ)、使用するサーバーIPを返します。選択は任意にすることができます。サーバーのステータスはクエリで変化するため、どのスタンバイが選択されているかは関係ありません。
クエリを1つの更新に制限することはできますか?
ここに私がこれまでに持っているものがあります:
UPDATE server_info SET status = 'active'
WHERE status = 'standby' [[LIMIT 1???]]
RETURNING server_ip;
Postgresはこれが好きではありません。別に何ができるでしょうか?
[〜#〜] cte [〜#〜](Common Table Expressions) で選択を具体化し、それのFROM
句でそれに結合します UPDATE
。
_WITH cte AS (
SELECT server_ip -- pk column or any (set of) unique column(s)
FROM server_info
WHERE status = 'standby'
LIMIT 1 -- arbitrary pick (cheapest)
)
UPDATE server_info s
SET status = 'active'
FROM cte
WHERE s.server_ip = cte.server_ip
RETURNING s.server_ip;
_
私はここに単純なサブクエリを最初に持っていましたが、 Feike が指摘するように、特定のクエリプランのLIMIT
を回避できます。
プランナは、
LIMITing
サブクエリに対してネストされたループを実行するプランを生成することを選択できます。これにより、UPDATEs
より多くのLIMIT
が発生します。
_Update on buganalysis [...] rows=5 -> Nested Loop -> Seq Scan on buganalysis -> Subquery Scan on sub [...] loops=11 -> Limit [...] rows=2 -> LockRows -> Sort -> Seq Scan on buganalysis
_上記を修正する方法は、
LIMIT
サブクエリを独自のCTEでラップすることでした。CTEが具体化されると、ネストされたループの異なる反復で異なる結果を返さなくなります。
Or控えめに使用correlatedサブクエリLIMIT
_1
_の単純なケース。シンプルで高速:
_UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
_
このすべてについて デフォルトの分離レベル_READ COMMITTED
_ と仮定します。より厳密な分離レベル(_REPEATABLE READ
_およびSERIALIZABLE
)でも、シリアル化エラーが発生する可能性があります。見る:
同時書き込み負荷の下で、_FOR UPDATE SKIP LOCKED
_を追加して行をロックし、競合状態を回避します。 _SKIP LOCKED
_がPostgresに追加されました9.5、古いバージョンについては以下を参照してください。 マニュアル:
_
SKIP LOCKED
_を使用すると、すぐにロックできない選択された行はスキップされます。ロックされた行をスキップすると、データの一貫性のないビューが提供されるため、これは汎用作業には適していませんが、キューのようなテーブルにアクセスする複数のコンシューマとのロック競合を回避するために使用できます。
_UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING server_ip;
_
条件を満たす、ロックされていない行が残っていない場合、このクエリでは何も行われず(行は更新されません)、空の結果が得られます。重要ではない操作の場合、これで完了です。
ただし、並行トランザクションでは行がロックされている可能性がありますが、更新は完了しません(ROLLBACK
またはその他の理由)。 念のため最終チェックを実行:
_SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
_
SELECT
はロックされた行も表示します。 true
を返さない場合、1つ以上の行がまだ処理されており、トランザクションは引き続きロールバックされる可能性があります。 (その間、新しい行が追加されます。)少し待ってから、2つのステップをループします:(UPDATE
行が返されなくなるまで; SELECT
...)true
。
関連:
SKIP LOCKED
_なし9.4以前_UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
_
同じ行をロックしようとする並行トランザクションは、最初のトランザクションがロックを解放するまでブロックされます。
最初のトランザクションがロールバックされた場合、次のトランザクションがロックを取得して通常どおり続行します。キュー内の他のメンバーは待機し続けます。
最初にコミットされた場合、WHERE
条件は再評価され、それがTRUE
でない場合(status
が変更された場合)、CTEは(少し意外にも)行を返しません。何も起こりません。すべてのトランザクションがthesamerowを更新したい場合、これは望ましい動作です。
ただし、各トランザクションが更新する必要がある場合はthenextrow。 arbitrary(またはrandom)行を更新したいだけなので、待つのは全く意味がありません。
アドバイザリロック を使用して、状況のブロックを解除できます。
_UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
AND pg_try_advisory_xact_lock(id)
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
_
このようにして、まだロックされていない次の行が更新されます。各トランザクションは、処理する新しい行を取得します。私はこのトリックのために Czech Postgres Wiki の助けを借りました。
id
は任意の一意のbigint
列(または_int4
_または_int2
_などの暗黙のキャストを持つ任意の型)です。
アドバイザリロックがデータベース内の複数のテーブルで同時に使用されている場合は、ここでpg_try_advisory_xact_lock(tableoid::int, id)
-id
を一意のinteger
として明確にします。tableoid
はbigint
量であるため、理論的にはinteger
でオーバーフローする可能性があります。偏執狂的である場合は、代わりに_(tableoid::bigint % 2147483648)::int
_を使用してください-真の妄想性のために理論的な「ハッシュ衝突」を残します...
また、PostgresはWHERE
条件を任意の順序で自由にテストできます。couldpg_try_advisory_xact_lock()
をテストしてロックを取得before_status = 'standby'
_、 _status = 'standby'
_が真でない場合、無関係な行に追加のアドバイザリロックが発生する可能性があります。 SOに関する関連質問:
通常、これは無視できます。guarantee条件を満たす行のみがロックされるようにするには、上記のようなCTEまたは _OFFSET 0
_ハック(インライン化を防止) 。例:
または(順次スキャンの方が安い)CASE
ステートメントの条件を次のようにネストします。
_WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
_
ただしCASE
トリックは、Postgresがstatus
のインデックスを使用しないようにします。このようなインデックスが利用可能な場合、最初に追加のネストを行う必要はありません。インデックススキャンでは、条件を満たす行のみがロックされます。
すべての呼び出しでインデックスが使用されているかどうかを確認することはできないので、次のようにすることができます。
_WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
_
CASE
は論理的に冗長ですが、説明した目的を果たします。
コマンドが長いトランザクションの一部である場合は、手動で解放できる(そして解放する必要がある)セッションレベルのロックを検討してください。したがって、ロックされた行を使い終わったらすぐにロックを解除できます: pg_try_advisory_lock()
and pg_advisory_unlock()
。 マニュアル:
セッションレベルで取得すると、通知ロックは明示的に解放されるかセッションが終了するまで保持されます。
関連: