web-dev-qa-db-ja.com

PostgresでのUPDATE / INSERTの組み合わせのロック

テーブルが2つあります。 1つはログテーブルです。もう1つには、基本的に1回しか使用できないクーポンコードが含まれています。

ユーザーはクーポンを利用できる必要があります。これにより、ログテーブルに行が挿入され、クーポンが使用済みとしてマークされます(used列をtrueに更新することにより)。

当然、ここには明らかな競合状態/セキュリティ問題があります。

私は過去にもmySQLの世界で同様のことをしたことがあります。その世界では、両方のテーブルをグローバルにロックし、これは一度に1回だけ発生する可能性があることを認識してロジックを安全に実行し、完了したらテーブルのロックを解除します。

Postgresでこれを行うより良い方法はありますか?特に、ロックがグローバルであることに懸念がありますが、グローバルである必要はありません。他のユーザーがその特定のコードを入力しようとしていないことを確認するだけでよいので、行レベルのロックが機能する可能性があります。

11
Rob Miller

MySQLのような同時実行性の問題については以前に聞いたことがあります。 Postgresではそうではありません。

デフォルトの組み込み行レベルロック READ COMMITTEDトランザクション分離レベル で十分です。

データを変更するCTE(MySQLにもないもの)を1つのステートメントで提案することをお勧めします。これは、(必要な場合に)あるテーブルから別のテーブルに直接値を渡すのが便利だからです。 couponテーブルの内容が必要ない場合は、UPDATEおよびINSERTステートメントを個別に使用してトランザクションを使用することもできます。

WITH upd AS (
   UPDATE coupon
   SET    used = true
   WHERE  coupon_id = 123
   AND    NOT used
   RETURNING coupon_id, other_column
   )
INSERT INTO log (coupon_id, other_column)
SELECT coupon_id, other_column FROM upd;

複数のトランザクションが同じクーポンを引き換えようとするのはまれなものでなければなりません。彼らにはユニークな番号がありますね。同時に複数のトランザクションが同時に試行されることは、はるかにまれです。 (たぶん、アプリケーションのバグか、システムをゲームしようとしている誰か?)

とはいえ、UPDATE1つだけトランザクションに対してのみ成功し、何があっても。 UPDATEは、更新前に各ターゲット行で行レベルのロックを取得します。並行トランザクションが同じ行をUPDATEしようとすると、行のロックが表示され、ブロッキングトランザクションが完了するまで待機します(ROLLBACKまたはCOMMIT)。ロックキューの最初:

  • コミットした場合は、状態を再確認してください。それでもNOT usedの場合は、行をロックして続行します。そうでない場合、UPDATEは対象となる行を検出せず、nothingを実行して行を返しませんなので、INSERTも何もしません。

  • ロールバックした場合は、行をロックして続行します。

競合状態の可能性はありません

同じトランザクションに書き込みを追加するか、1つのトランザクションよりも多くの行をロックしない限り、 デッドロックの可能性はありません。 。

INSERTは気にしないでください。誤ってcoupon_idが既にlogテーブルにある場合(そしてlog.coupon_idにUNIQUEまたはPK制約がある場合)、一意の違反の後にトランザクション全体がロールバックされます。 DBでの不正な状態を示します。上記のステートメントがlogテーブルに書き込む唯一の方法である場合、それが発生することはありません。

15