データのクエリーを実行してから、同じトランザクション内で関連データを挿入または更新する場合は、通常の SELECT
ステートメントで十分な保護が提供されません。ほかのトランザクションは、クエリーが実行されたばかりの同じ行を更新または削除できます。InnoDB
では、追加の安全性が提供される 2 つのタイプのロック読み取りがサポートされています。
SELECT ... LOCK IN SHARE MODE
は、読み取られるすべての行に共有モードのロックを設定します。ほかのセッションもその行を読み取ることができますが、トランザクションがコミットするまで変更することはできません。これらの行のいずれかがコミットされていない別のトランザクションによって変更された場合、クエリーはそのトランザクションが終了するまで待機してから、最新の値を使用します。検索でインデックスレコードが見つかった場合、
SELECT ... FOR UPDATE
は、行および関連付けられたすべてのエントリをロックします。この動作は、これらの行にUPDATE
ステートメントを発行した場合と同じです。ほかのトランザクションは、これらの行の更新、SELECT ... LOCK IN SHARE MODE
の実行、または特定のトランザクション分離レベルでのデータの読み取りからブロックされます。一貫性読み取りでは、読み取られたビュー内に存在するレコードに設定されたロックはすべて無視されます。(古いバージョンのレコードはロックできません。レコードのインメモリーコピー上の Undo ログに適用することで、再構築されます。)
これらの句は、主に、単一のテーブル内または複数のテーブルに分割された状態で、ツリー構造またはグラフ構造のデータを処理する際に役立ちます。エッジまたはツリー分岐をある場所から別の場所にトラバースしても、これらの「ポインタ」に戻ってその値を変更する権利を保有しています。
トランザクションがコミットまたはロールバックされると、LOCK IN SHARE MODE
および FOR UPDATE
クエリーで設定されたすべてのロックが解放されます。
SELECT FOR UPDATE
を使用した更新対象の行のロックは、START TRANSACTION
でトランザクションを開始するか、autocommit
を 0 に設定することで、自動コミットが無効になっている場合にのみ適用されます。自動コミットが有効になっている場合は、指定に一致する行がロックされません。
使用例
child
テーブルに新しい行を挿入し、子の行が parent
テーブル内に親の行を持っていることを確認すると仮定します。アプリケーションコードを使用して、この操作シーケンス全体の参照整合性を確保できます。
まず、一貫性読み取りを使用して、PARENT
テーブルでクエリーを実行し、親の行が存在することを確認します。CHILD
テーブルに子の行を安全に挿入できますか。気付かないうちに、その他の一部のセッションで、SELECT
と INSERT
との間に親の行が削除された可能性もあるため、できません。
このような問題の可能性を回避するには、LOCK IN SHARE MODE
を使用して SELECT
を実行します。
SELECT * FROM parent WHERE NAME = 'Jones' LOCK IN SHARE MODE;
LOCK IN SHARE MODE
クエリーから「Jones」
という親が返されたら、CHILD
テーブルに子のレコードを安全に追加し、トランザクションをコミットできます。PARENT
テーブル内のアプリケーション行で読み取りまたは書き込みを行おうとするトランザクションは、ユーザーが完了するまで (つまり、すべてのテーブル内のデータが一貫性のある状態になるまで) 待機します。
もう 1 つの例では、CHILD
テーブルに追加された各子に一意の識別子を割り当てる際に使用される CHILD_CODES
テーブル内の整数カウンタフィールドを検討します。一貫性読み取りまたは共有モード読み取りを使用すると、データベースの 2 人のユーザーが同じカウンタ値を参照する可能性があり、2 つのトランザクションが同じ識別子を持つ行を CHILD
テーブルに追加しようとすると、重複キーのエラーが発生するため、カウンタの現在の値を読み取る際には使用しないでください。
ここで、2 人のユーザーがカウンタを同時に読み取る場合、少なくとも 1 人のユーザーがカウンタを更新しようとするとデッドロックが発生するため、LOCK IN SHARE MODE
は適切な解決策ではありません。
カウンタの読み取りおよび増分を実装するには、まず FOR UPDATE
を使用してカウンタのロック読み取りを実行してから、カウンタを増分します。例:
SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;
SELECT ... FOR UPDATE
は使用可能な最新データを読み取り、読み取られる各行上に排他ロックを設定します。したがって、検索された SQL UPDATE
によって行上に設定される場合と同じロックが設定されます。
前述の説明は、単に SELECT ... FOR UPDATE
がどのように機能するのかを示した例です。MySQL では、テーブルへの単一アクセスを使用するだけで、一意の識別子を生成する特定のタスクを実現できます。
UPDATE child_codes SET counter_field = LAST_INSERT_ID(counter_field + 1);
SELECT LAST_INSERT_ID();
この SELECT
ステートメントは、単に (現在の接続に固有の) 識別子情報を取得するだけです。どのテーブルにもアクセスしません。