web-dev-qa-db-ja.com

Postgresqlでロック解除された行を選択します

ロックされていないPostgresqlの行を選択する方法はありますか?私は次のことを行うマルチスレッドアプリを持っています:

Select... order by id desc limit 1 for update

テーブルの上。

複数のスレッドがこのクエリを実行する場合、両方が同じ行をプルバックしようとします。

1つは行ロックを取得し、他はブロックし、最初のブロックが行を更新した後に失敗します。私が本当に望んでいるのは、2番目のスレッドがWHERE句に一致し、まだロックされていない最初の行を取得することです。

明確にするために、選択を実行した後、各スレッドが最初に使用可能な行をすぐに更新するようにします。

したがって、ID: 1,2,3,4の行がある場合は、最初のスレッドが入り、ID=4の行を選択して、すぐに更新します。

そのトランザクション中に2番目のスレッドが来た場合、ID=3で行を取得し、すぐにその行を更新したいと思います。

nowait句はロックされた行(ID=4 in my example)と一致するため、ShareではこれもWHEREでも実行されません。基本的に私が欲しいのは、WHERE句の「ANDNOTLOCKED」のようなものです。

Users

-----------------------------------------
ID        | Name       |      flags
-----------------------------------------
1         |  bob       |        0
2         |  fred      |        1
3         |  tom       |        0
4         |  ed        |        0

クエリが「Select ID from users where flags = 0 order by ID desc limit 1」で、行が返されたときに次のものが「Update Users set flags = 1 where ID = 0」である場合、最初のスレッドでID 4を使用して行を取得し、次のスレッドを取得します。 ID 3で行を取得するために1つ。

選択に「For Update」を追加すると、最初のスレッドが行を取得し、2番目のスレッドがブロックして、最初のトランザクションがコミットするとWHERE句が満たされないため、何も返されません。

For Update」を使用しない場合は、後続の更新(WHEREフラグ= 0)でWHERE句を追加して、1つのスレッドのみが行を更新できるようにする必要があります。

2番目のスレッドは最初のスレッドと同じ行を選択しますが、2番目のスレッドの更新は失敗します。

いずれにせよ、トランザクションが重複する最初のスレッドに行4を、2番目のスレッドに行3を与えるデータベースを取得できないため、2番目のスレッドは行の取得と更新に失敗します。

42
alanc10n

この機能、 SELECT ... SKIP LOCKEDはPostgres9.5で実装されています。 http://www.depesz.com/2014/10/10/waiting-for-9-5-implement-skip-locked-for-row-level-locks/

27
Gavin Wahl

いいえいいえNOOO :-)

著者の意味を知っています。私も同様の状況にあり、素晴らしい解決策を思いつきました。まず、自分の状況を説明することから始めます。特定の時間に送信する必要のあるメッセージを格納するテーブルiがあります。 PGは関数のタイミング実行をサポートしていないため、デーモン(またはcron)を使用する必要があります。いくつかの並列プロセスを開くカスタム記述スクリプトを使用します。すべてのプロセスは、+ 1秒/ -1秒の精度で送信する必要があるメッセージのセットを選択します。テーブル自体は、新しいメッセージで動的に更新されます。

したがって、すべてのプロセスで一連の行をダウンロードする必要があります。この行のセットは、多くの混乱を招くため、他のプロセスではダウンロードできません(1つだけを受信する必要があるときに、カップルのメッセージを受信する人もいます)。そのため、行をロックする必要があります。ロック付きの一連のメッセージをダウンロードするためのクエリ:

FOR messages in select * from public.messages where sendTime >= CURRENT_TIMESTAMP - '1 SECOND'::INTERVAL AND sendTime <= CURRENT_TIMESTAMP + '1 SECOND'::INTERVAL AND sent is FALSE FOR UPDATE LOOP
-- DO SMTH
END LOOP;

このクエリを使用するプロセスは、0.5秒ごとに開始されます。したがって、これにより、次のクエリは最初のロックが行のロックを解除するのを待機します。このアプローチでは、大幅な遅延が発生します。 NOWAITを使用する場合でも、送信する必要のある新しいメッセージがテーブルにある可能性があるため、クエリによって不要な例外が発生します。単にFORSHAREを使用すると、クエリは適切に実行されますが、それでも大きな遅延が発生するまでに多くの時間がかかります。

それを機能させるために、私たちは少し魔法をかけます:

  1. クエリの変更:

    FOR messages in select * from public.messages where sendTime >= CURRENT_TIMESTAMP - '1 SECOND'::INTERVAL AND sendTime <= CURRENT_TIMESTAMP + '1 SECOND'::INTERVAL AND sent is FALSE AND is_locked(msg_id) IS FALSE FOR SHARE LOOP
    -- DO SMTH
    END LOOP;
    
  2. 不思議な関数 'is_locked(msg_id)'は次のようになります。

    CREATE OR REPLACE FUNCTION is_locked(integer) RETURNS BOOLEAN AS $$
    DECLARE
        id integer;
        checkout_id integer;
        is_it boolean;
    BEGIN
        checkout_id := $1;
        is_it := FALSE;
    
        BEGIN
            -- we use FOR UPDATE to attempt a lock and NOWAIT to get the error immediately 
            id := msg_id FROM public.messages WHERE msg_id = checkout_id FOR UPDATE NOWAIT;
            EXCEPTION
                WHEN lock_not_available THEN
                    is_it := TRUE;
        END;
    
        RETURN is_it;
    
    END;
    $$ LANGUAGE 'plpgsql' VOLATILE COST 100;
    

もちろん、この関数をカスタマイズして、データベースにある任意のテーブルで機能するようにすることができます。私の意見では、1つのテーブルに対して1つのチェック関数を作成する方が良いと思います。この関数にさらに多くのものを追加すると、遅くなるだけです。とにかくこの句をチェックするのに時間がかかるので、さらに遅くする必要はありません。私にとってこれは完全なソリューションであり、完全に機能します。

これで、50個のプロセスを並行して実行すると、すべてのプロセスに送信する新しいメッセージの一意のセットがあります。送信されたら、sent = TRUEで行を更新するだけで、二度とその行に戻ることはありません。

この解決策があなた(著者)にも役立つことを願っています。ご不明な点がございましたら、お気軽にお問い合わせください:-)

ああ、これがあなたにもうまくいったかどうか教えてください。

9
Marek

私はこのようなものを使用します:

select  *
into l_sms
from sms
where prefix_id = l_prefix_id
    and invoice_id is null
    and pg_try_advisory_lock(sms_id)
order by suffix
limit 1;

pg_advisory_unlockを呼び出すことを忘れないでください

6
Timon

キューを実装しようとしている場合は、この問題やその他の問題をすでに解決しているPGQを確認してください。 http://wiki.postgresql.org/wiki/PGQ_Tutorial

5

別のプロセスによってまだ処理されていないキュー内の最も優先度の高いアイテムを取得するようなことをしようとしているようです。

考えられる解決策は、未処理のリクエストに限定するwhere句を追加することです。

select * from queue where flag=0 order by id desc for update;
update queue set flag=1 where id=:id;
--if you really want the lock:
select * from queue where id=:id for update;
...

うまくいけば、フラグの更新が行われている間、2番目のトランザクションがブロックされ、続行できますが、フラグによって次の行に制限されます。

また、シリアル化可能な分離レベルを使用すると、この狂気のすべてなしで、必要な結果を得ることができる可能性があります。

アプリケーションの性質によっては、FIFOまたはLIFOパイプなど、データベースよりも優れた実装方法がある場合があります。さらに、必要な順序を逆にし、シーケンスを使用して、それらが順番に処理されるようにすることができる場合があります。

2
Grant Johnson

これは、SELECT ... NOWAITによって実行できます。例は ここ です。

1
radek

より良い答えがまだ見つからないため、アプリ内でロックを使用して、このクエリを実行するコードへのアクセスを同期することにしました。

0
alanc10n

私の解決策は、RETURNING句を指定してUPDATEステートメントを使用することです。

Users

-----------------------------------
ID        | Name       |      flags
-----------------------------------
1         |  bob       |        0  
2         |  fred      |        1  
3         |  tom       |        0   
4         |  ed        |        0   

の代わりに SELECT .. FOR UPDATE 使用する

BEGIN; 

UPDATE "Users"
SET ...
WHERE ...;
RETURNING ( column list );

COMMIT;

UPDATEステートメントはテーブルのROWEXCLUSIVEロックを取得するため、その更新により、シリアル化された更新が取得されます。読み取りは引き続き許可されますが、UPDATEトランザクションの開始前にのみデータが表示されます。

参照: 同時実行制御 ページドキュメントの章。

0
Ketema

マルチスレッドとクラスターで使用されますか?
これはどう?

START TRANSACTION;

// All thread retrive same task list
// If result count is very big, using cursor 
//    or callback interface provied by ORM frameworks.
var ids = SELECT id FROM tableName WHERE k1=v1;

// Each thread get an unlocked recored to process.
for ( id in ids ) {
   var rec = SELECT ... FROM tableName WHERE id =#id# FOR UPDATE NOWAIT;
   if ( rec != null ) {
    ... // do something
   }
}

COMMIT;
0
btpka3

^^それはうまくいきます。 「ロック済み」の「即時」ステータスを持つことを検討してください。

あなたのテーブルがそのようなものだとしましょう:

id |名前|姓|状態

たとえば、考えられるステータスは次のとおりです。1=保留中、2 =ロック済み、3 =処理済み、4 =失敗、5 =拒否

すべての新しいレコードは、ステータスが保留中(1)で挿入されます

あなたのプログラムは次のことを行います: "update mytable set status = 2 where id =(select id from mytable where name like '%John%' and status = 1 limit 1)returning id、name、surname"

次に、プログラムがその処理を実行し、このスレッドがその行をまったく処理してはならないという結論に達した場合、「update mytable set status = 1 where id =?」を実行します。

それ以外では、他のステータスに更新されます。

0
Kostas

SELECT FORSHAREを探しているようです。

http://www.postgresql.org/docs/8.3/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE

FOR SHAREは、取得された各行で排他的ロックではなく共有ロックを取得することを除いて、同様に動作します。共有ロックは、他のトランザクションがこれらの行に対してUPDATE、DELETE、またはSELECT FOR UPDATEを実行するのをブロックしますが、SELECT FORSHAREの実行を妨げることはありません。

特定のテーブルがFORUPDATEまたはFORSHAREで指定されている場合、それらのテーブルからの行のみがロックされます。 SELECTで使用される他のテーブルは、通常どおりに読み取られます。テーブルリストのないFORUPDATEまたはFORSHARE句は、コマンドで使用されるすべてのテーブルに影響します。 FORUPDATEまたはFORSHAREがビューまたはサブクエリに適用されると、ビューまたはサブクエリで使用されるすべてのテーブルに影響します。

テーブルごとに異なるロック動作を指定する必要がある場合は、複数のFORUPDATE句とFORSHARE句を記述できます。同じテーブルがFORUPDATE句とFORSHARE句の両方によって言及されている(または暗黙的に影響を受けている)場合、そのテーブルはFORUPDATEとして処理されます。同様に、テーブルに影響を与える句のいずれかで指定されている場合、テーブルはNOWAITとして処理されます。

FORUPDATEおよびFORSHAREは、返された行を個々のテーブル行で明確に識別できないコンテキストでは使用できません。たとえば、集計では使用できません。

0
Steven Behnke

私はアプリケーションで同じ問題に直面し、GrantJohnsonのアプローチと非常によく似た解決策を思いつきました。 A FIFOまたはLIFOパイプは、1つのDBにアクセスするアプリケーションサーバーのクラスターがあるため、オプションではありませんでした。

SELECT ... WHERE FLAG=0 ... FOR UPDATE
UPDATE ... SET FLAG=1 WHERE ID=:id
0
woeye

何を達成しようとしていますか?ロック解除された行の更新も完全なトランザクションもあなたが望むことをしない理由をよりよく説明できますか?

さらに良いことに、競合を防ぎ、各スレッドに異なるオフセットを使用させることができますか?テーブルの関連部分が頻繁に更新されている場合、これはうまく機能しません。まだ衝突が発生しますが、インサートの負荷が大きい場合のみです。

Select... order by id desc offset THREAD_NUMBER limit 1 for update
0
HUAGHAGUAH

次はどうですか? 可能性があります他の例よりもアトミックに扱われますが、それでもテストして、私の仮定が間違っていないことを確認する必要があります。

UPDATE users SET flags = 1 WHERE id = ( SELECT id FROM users WHERE flags = 0 ORDER BY id DESC LIMIT 1 ) RETURNING ...;

同時UPDATEに直面しても、postgresが内部で一貫したSELECT結果を提供するために使用するロックスキームに固執する可能性があります。

0
HUAGHAGUAH