sql >> データベース >  >> RDS >> Mysql

MySQLアップデートクエリ-競合状態と行ロックで「where」条件は尊重されますか? (php、PDO、MySQL、InnoDB)

    レース中は場所の条件が尊重されますが、誰がレースに勝ったかを確認する方法に注意する必要があります。

    これがどのように機能し、なぜ注意しなければならないのかについて、次のデモンストレーションを検討してください。

    まず、いくつかの最小限のテーブルを設定します。

    CREATE TABLE table1 (
    `id` TINYINT UNSIGNED NOT NULL PRIMARY KEY,
    `locked` TINYINT UNSIGNED NOT NULL,
    `updated_by_connection_id` TINYINT UNSIGNED DEFAULT NULL
    ) ENGINE = InnoDB;
    
    CREATE TABLE table2 (
    `id` TINYINT UNSIGNED NOT NULL PRIMARY KEY
    ) ENGINE = InnoDB;
    
    INSERT INTO table1
    (`id`,`locked`)
    VALUES
    (1,0);
    

    id idの役割を果たします テーブル内のupdated_by_connection_id assignedPhoneのように機能します 、およびlocked reservationCompletedのように 。

    それでは、レーステストを始めましょう。 2つのコマンドライン/ターミナルウィンドウを開き、mysqlに接続し、これらのテーブルを作成したデータベースを使用する必要があります。

    接続1

    start transaction;
    

    接続2

    start transaction;
    

    接続1

    UPDATE table1
    SET locked = 1,
    updated_by_connection_id = 1
    WHERE id = 1
    AND locked = 0;
    

    接続2

    UPDATE table1
    SET locked = 1,
    updated_by_connection_id = 2
    WHERE id = 1
    AND locked = 0;
    

    接続2は現在待機中です

    接続1

    SELECT * FROM table1 WHERE id = 1;
    
    commit;
    

    この時点で、接続2が解放されて続行し、以下を出力します。

    接続2

    SELECT * FROM table1 WHERE id = 1;
    
    commit;
    

    すべてがうまく見えます。はい、WHERE句はレースの状況で尊重されていたことがわかります。

    ただし、注意が必要だと言ったのは、実際のアプリケーションでは、物事が必ずしもこれほど単純ではないためです。トランザクション内で他のアクションが実行されている可能性があり、それによって実際に結果が変わる可能性があります。

    次のようにしてデータベースをリセットしましょう:

    delete from table1;
    INSERT INTO table1
    (`id`,`locked`)
    VALUES
    (1,0);
    

    そして今、この状況を考えてみましょう。この状況では、UPDATEの前にSELECTが実行されます。

    接続1

    start transaction;
    
    SELECT * FROM table2;
    

    接続2

    start transaction;
    
    SELECT * FROM table2;
    

    接続1

    UPDATE table1
    SET locked = 1,
    updated_by_connection_id = 1
    WHERE id = 1
    AND locked = 0;
    

    接続2

    UPDATE table1
    SET locked = 1,
    updated_by_connection_id = 2
    WHERE id = 1
    AND locked = 0;
    

    接続2は現在待機中です

    接続1

    SELECT * FROM table1 WHERE id = 1;
    
    SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
    
    commit;
    

    この時点で、接続2が解放されて続行し、以下を出力します。

    さて、誰が勝ったか見てみましょう:

    接続2

    SELECT * FROM table1 WHERE id = 1;
    

    待って、何? lockedのはなぜですか 0およびupdated_by_connection_id NULL ??

    これは私が言及した注意です。犯人は、実際には最初に選択を行ったという事実によるものです。正しい結果を得るには、次のコマンドを実行します。

    SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
    
    commit;
    

    SELECT ... FOR UPDATEを使用すると、正しい結果を得ることができます。 SELECTとSELECT...FOR UPDATEは2つの異なる結果をもたらすため、これは非常に混乱する可能性があります(元々は私にとってそうでした)。

    これが発生する理由は、デフォルトの分離レベルREAD-REPEATABLEが原因です。 。最初のSELECTが行われたとき、start transaction;の直後 、スナップショットが作成されます。今後の更新されない読み取りはすべて、そのスナップショットから実行されます。

    したがって、更新を行った後で単純にSELECTを実行すると、の元のスナップショットから情報が取得されます。 行が更新されました。 SELECT ... FOR UPDATEを実行すると、正しい情報を取得するように強制されます。

    ただし、実際のアプリケーションでは、これが問題になる可能性があります。たとえば、リクエストがトランザクションにラップされており、更新を実行した後、いくつかの情報を出力するとします。その情報を収集して出力することは、「万が一に備えて」FORUPDATE句を散らかしたくない個別の再利用可能なコードによって処理される可能性があります。それは不必要なロックのために多くの欲求不満につながるでしょう。

    代わりに、別のトラックを使用することをお勧めします。ここには多くのオプションがあります。

    1つは、UPDATEの完了後にトランザクションをコミットすることを確認することです。ほとんどの場合、これがおそらく最良で最も単純な選択です。

    もう1つのオプションは、SELECTを使用して結果を判別しようとしないことです。代わりに、影響を受ける行を読み取り、それを使用して(1行の更新と0行の更新)、UPDATEが成功したかどうかを判断できる場合があります。

    別のオプションであり、単一のリクエスト(HTTPリクエストなど)を単一のトランザクションで完全にラップしたままにするために頻繁に使用するオプションは、トランザクションで実行される最初のステートメントがUPDATEのいずれかであることを確認することです。またはSELECT...FOR UPDATE 。これにより、接続の続行が許可されるまでスナップショットが作成されません。

    テストデータベースをもう一度リセットして、これがどのように機能するかを見てみましょう。

    delete from table1;
    INSERT INTO table1
    (`id`,`locked`)
    VALUES
    (1,0);
    

    接続1

    start transaction;
    
    SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
    

    接続2

    start transaction;
    
    SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
    

    接続2は現在待機中です。

    接続1

    UPDATE table1
    SET locked = 1,
    updated_by_connection_id = 1
    WHERE id = 1
    AND locked = 0;
    
    SELECT * FROM table1 WHERE id = 1;
    
    SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
    
    commit;
    

    接続2が解放されました。

    接続2

    +----+--------+--------------------------+
    | id | locked | updated_by_connection_id |
    +----+--------+--------------------------+
    |  1 |      1 |                        1 |
    +----+--------+--------------------------+
    

    ここでは、サーバー側のコードでこのSELECTの結果を実際にチェックして、それが正確であることを確認し、次の手順に進まないようにすることができます。ただし、完全を期すために、前と同じように終了します。

    UPDATE table1
    SET locked = 1,
    updated_by_connection_id = 2
    WHERE id = 1
    AND locked = 0;
    
    SELECT * FROM table1 WHERE id = 1;
    
    SELECT * FROM table1 WHERE id = 1 FOR UPDATE;
    
    commit;
    

    これで、接続2でSELECTとSELECT ...FORUPDATEが同じ結果になることがわかります。これは、SELECTが読み取るスナップショットが、接続1がコミットされるまで作成されなかったためです。

    したがって、元の質問に戻ります。はい、すべての場合において、WHERE句はUPDATEステートメントによってチェックされます。ただし、そのUPDATEの結果を誤って判断しないように、実行している可能性のあるSELECTには注意する必要があります。

    (はい、別のオプションはトランザクション分離レベルを変更することです。ただし、これと存在する可能性のあるゴッチャの経験はあまりないので、ここでは説明しません。)



    1. MySQLINクエリで順序を維持する

    2. 検索クエリでハイフンを使用した全文検索を許可する方法

    3. Djangoのタイムゾーン設定がエポックタイムに影響するのはなぜですか?

    4. Oracleの複数の行の列値を連結するSQLクエリ