web-dev-qa-db-ja.com

SQLアトミックインクリメントとロック戦略-これは安全ですか?

SQLとロック戦略について質問があります。例として、自分のWebサイトに画像のビューカウンターがあるとします。次のステートメントを実行するためのsprocなどがある場合:

START TRANSACTION;
UPDATE images SET counter=counter+1 WHERE image_id=some_parameter;
COMMIT;

特定のimage_idのカウンターの値が時間t0で「0」であると想定します。同じイメージカウンターs1とs2を更新する2つのセッションがt0で同時に開始する場合、これら2つのセッションが両方とも値「0」を読み取り、値を「1」に増やし、両方がカウンターを「1」に更新しようとする可能性があります。 '、したがって、カウンターは' 2 'ではなく' 1 'の値を取得しますか?

s1: begin
s1: begin
s1: read counter for image_id=15, get 0, store in temp1
s2: read counter for image_id=15, get 0, store in temp2
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1
s1: commit, ok
s2: commit, ok

最終結果:image_id = 15の誤った値「1」は2であるはずです。

私の質問は次のとおりです。

  1. このシナリオは可能ですか?
  2. もしそうなら、トランザクション分離レベルは重要ですか?
  3. このような競合をエラーとして検出する競合リゾルバーはありますか?
  4. 問題を回避するために特別な構文を使用できますか(コンペアアンドスワップ(CAS)や明示的なロック手法など)?

私は一般的な答えに興味がありますが、MySqlとInnoDB固有の答えがない場合は、この手法を使用してInnoDBにシーケンスを実装しようとしているため、興味があります。

編集:次のシナリオも可能であり、同じ動作になります。分離レベルREAD_COMMITED以上であると想定しているため、s1はすでにカウンターに「1」を書き込んでいますが、s2はトランザクションの開始から値を取得します。

s1: begin
s1: begin
s1: read counter for image_id=15, get 0, store in temp1
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: read counter for image_id=15, get 0 (since another tx), store in temp2
s2: write counter for image_id=15 to (temp2+1), which is also 1
s1: commit, ok
s2: commit, ok
43

UPDATEクエリは、読み取ったページまたはレコードに更新ロックを設定します。

レコードを更新するかどうかが決定されると、ロックが解除されるか、排他ロックに昇格されます。

これは、このシナリオでは次のことを意味します。

s1: read counter for image_id=15, get 0, store in temp1
s2: read counter for image_id=15, get 0, store in temp2
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1

s2は、s1がカウンターを書き込むかどうかを決定するまで待機しますが、このシナリオは実際には不可能です。

これになります:

s1: place an update lock on image_id = 15
s2: try to place an update lock on image_id = 15: QUEUED
s1: read counter for image_id=15, get 0, store in temp1
s1: promote the update lock to the exclusive lock
s1: write counter for image_id=15 to (temp1+1), which is 1 
s1: commit: LOCK RELEASED
s2: place an update lock on image_id = 15
s2: read counter for image_id=15, get 1, store in temp2
s2: write counter for image_id=15 to (temp2+1), which is 2

InnoDBでは、DMLクエリは、読み取ったレコードから更新ロックを解除しないことに注意してください。

これは、全表スキャンの場合、読み取られたが更新しないと決定されたレコードは、トランザクションが終了するまでロックされたままであり、別のトランザクションから更新できないことを意味します。

30
Quassnoi

ロックが適切に行われない場合、このタイプの競合状態を取得することは確かに可能であり、デフォルトのロックモード(読み取りコミット)はそれを許可します。このモードでは、読み取りはレコードに共有ロックを設定するだけなので、0を確認し、インクリメントして、データベースに1を書き込むことができます。

この競合状態を回避するには、読み取り操作に排他ロックを設定する必要があります。 「Serializable」および「RepeatableRead」同時実行モードはこれを実行し、単一行での操作の場合、それらはほぼ同等です。

完全にアトミックにするには、次のことを行う必要があります。

  • Serializableなどの適切な トランザクション分離レベル を設定します。通常、これはクライアントライブラリまたはSQLの明示から行うことができます。
  • トランザクションを開始します
  • データを読む
  • 更新する
  • トランザクションをコミットします。

SQLダイアレクトに応じて、HOLDLOCK(T-SQL)または同等のヒントを使用して、読み取りに排他ロックを強制することもできます。

単一の更新クエリはこれをアトミックに実行しますが、読み取りが排他ロックを取得することを確認せずに操作を分割することはできません(おそらく値を読み取ってクライアントに返すため)。 シーケンスを実装するには、アトミックに値を取得する必要があるため、更新だけではおそらく必要なものがすべてではありません。 アトミック更新を使用しても、更新後に値を読み取るための競合状態があります。読み取りはトランザクション内で実行する必要があります(取得したものを変数に格納します)、読み取り中に排他ロックを発行します。

ホットスポットを作成せずにこれを行うには、データベースがストアドプロシージャ内で 自律(ネストされた)トランザクション を適切にサポートしている必要があることに注意してください。チェーントランザクションまたはセーブポイントを指すために「ネストされた」が使用されることがあるため、この用語は少し混乱する可能性があることに注意してください。自律型トランザクションを参照するようにこれを編集しました。

自律型トランザクションがない場合、ロックは親トランザクションに継承され、親トランザクションはロット全体をロールバックできます。これは、親トランザクションがコミットされるまで保持されることを意味します。これにより、シーケンスが、そのシーケンスを使用してすべてのトランザクションをシリアル化するホットスポットに変わる可能性があります。シーケンスを使用しようとする他のすべてのものは、親トランザクション全体がコミットされるまでブロックされます。

IIRC Oracleは自律型トランザクションをサポートしますが、DB/2はごく最近までサポートしておらず、SQLServerはサポートしていません。頭から離れて、InnoDBがそれらをサポートしているかどうかはわかりませんが、 Grey and Reuter 実装の難しさについては少し詳しく説明します。実際には、そうではない可能性が非常に高いと思います。 YMMV。