46

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
4

2 に答える 2

32

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

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

これは次のようになります。

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クエリは読み取ったレコードから更新ロックを解除しないことに注意してください。

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

于 2010-09-29T12:59:38.397 に答える
8

ロックが適切に行われないと、このタイプの競合状態が発生する可能性があり、デフォルトのロック モード (読み取りコミット) ではそれが可能です。このモードでは、読み取りはレコードに共有ロックを設定するだけなので、両方とも 0 を認識し、増分し、データベースに 1 を書き込むことができます。

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

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

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

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

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

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

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

IIRC Oracle は自律型トランザクションをサポートしていますが、DB/2 はごく最近までサポートしておらず、SQL Server もサポートしていません。頭のてっぺんから、InnoDB がそれらをサポートしているかどうかはわかりませんが、 Gray と Reuterは、それらの実装がどれほど難しいかについて、ある程度詳しく述べています。実際には、そうではない可能性が高いと思います。YMMV。

于 2010-09-29T12:47:13.570 に答える