web-dev-qa-db-ja.com

SELECT ... FOR UPDATEはロックを待った後に古い値を返します

SELECT ... FOR UPDATEはロックを待機する必要があり、その間に別のスレッドが結果を変更してコミットすると、最初のクエリはold結果を返します。つまり、before変更の前。

これは予想される動作ですか、それともバグですか?それは確かに役に立たないようです。

簡単に説明できます。次の表を見てみましょう。

CREATE TABLE `orders`(  
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `description` VARCHAR(50),
  PRIMARY KEY (`id`)
) ENGINE=INNODB;

そして、この一連のステートメント:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;

SELECT MAX(id)
FROM orders
FOR UPDATE;

# PAUSE HERE

INSERT INTO orders (description)
VALUES ('bla');

COMMIT;

今、私たちは次のことを行います:

  1. 2つの接続を開きます。
  2. 接続1では、一時停止まで実行します。これは成功します。
  3. 接続2では、一時停止まで実行します。これはロックを待機してスタックします。
  4. 接続1で、残りを実行します。これは成功します。
  5. 接続2が前のIDを返しました!
  6. 接続2で、もう一度SELECTを実行します。 Now更新された値を返します。

したがって、賢明で望ましいと思われる動作を実現するには、SELECTを2回実行する必要があるようです。

これは予想される動作ですか、それともバグですか?

(MySQL 5.7.25でテスト済み)

1
Timo

私はいくつかの実験を行うことによって、その振る舞いの説明を理解しました。新しい行を挿入するのではなく、接続1を使用して他のいくつかの変更を実行しました。結合された結果は、プロセスがどのように機能するかを明らかにします。

  • 接続1がdescriptionidを更新すると、接続2は更新されたdescriptionを参照します。 (索引スキャンは、ロックされた行の前に停止しました。)
  • 接続1がidsmaller値に更新する場合、たとえば、 10から9まで、接続2は新しい値9を返します(ロックされた行の前にインデックススキャンが停止し、行が消えることを確認し、降順で続行して、9に到達します)。
  • 接続1がidgreater値に更新した場合、たとえば、 10から11まで、接続2はその値をスキップ代わりに、元の値よりも小さい最大のidを返します。この例では、存在する場合はid 9を返します。 (インデックススキャンは、ロックされた行の前に停止し、行が消えることを確認し、降順で続行するため、11を通過することはなく、9に到達します。)
  • 接続1がidをより大きい値に更新した場合。 10から11に変更し、古い行の代わりに新しい行を挿入します。 id 10の場合、接続2は新しく挿入された行を選択します。 (インデックススキャンはロックされた行の前で停止しており、その場所に挿入されたものが表示されます。)
  • TODO:接続1がidをより大きい値に更新した場合。 10から20に変更してから、古い行の代わりに新しい行を挿入しますが、元の行よりも大きな値になります。 11、接続2はそれを見ますか?おそらくそうではありませんが、これはテストする必要があります。
  • 接続1がMAX(id)を10にロックし、接続2がハードコーディングされたidを9に選択した場合、ロックは重複せず、両方が独立して処理できます。

MAX(id)は逆スキャンを使用して決定され、ロックは実際の(インデックス)行にあると結論付けることができます。意味あり。分離レベルREAD COMMITTEDはギャップロックを取得しません。 REPEATABLE READ可能性がありますが、接続2のトランザクションが完全に失敗するため、そのシナリオをこれ以上詳しく調査することはありません。

正確な仕組みの結論

接続2は、特定の行がロックされるまで逆スキャンを開始します。

それはそのロックが解放されるのを待ち、それからスキャンを続行しますそれから(そしてそれを含めて)idが中断したもの。何でもないrowではなく、何でもid

  • その結果、そのidに行がなくなった場合、スキャンはその行を超えて、より小さなidsまで続行されます。
  • その結果、行が更新されている場合、更新された値が表示されます(id以外のものを選択した場合)。
  • その結果、行が移動され、そのidに新しい行が挿入された場合、その新しい行が選択されます。
1
Timo

これは予想される動作です。選択クエリを開始した場合、それらの結果をそのクエリ(またはトランザクション)の開始と一致させる必要があります。 FOR UPDATEを指定しているため、トランザクションの開始を示しています。コミットするとトランザクションが終了します。そのため、手順6で更新された値が返されます。

別のセッションが選択に関係する行を更新してコミットすると、セッションはロールバックデータを使用して、トランザクションの開始時のデータを再構築します。

0
Phil Sumner

単純に行うのに複雑さは必要ありません

_INSERT ...;
SELECT LAST_INSERT_ID();
_

LAST_INSERT_ID()の値は接続で保持されるため、他のアクションが実行されても影響を受けません。

なぜMAX(id)が必要なのですか?

ケース1:他のインサートで使用します。その後、トランザクションは完全に正常ですthis方法:

_BEGIN;
INSERT ...;
SELECT @id := LAST_INSERT_ID();  -- and put into local variable or @variable
INSERT something else ... VALUES (..., @id, ...);
COMMIT;
_

ケース2:さて、あなたのケースを説明してください。

0
Rick James