はじめに
MySQLでは、トランザクション処理におけるデータ整合性を保つために、複数の行レベルロックが内部的に使用されます。特にInnoDBストレージエンジンでは、「Next-Key Lock」「Gap Lock」「Record Lock」の3種類が密接に連携し、可読性と排他制御のバランスを実現します。これらのロックは単なる制御手段ではなく、データベースの並行性と一貫性を支える基盤です。
Next-Key Lock:範囲とレコードを同時に守るハイブリッドロック
Next-Key Lockは、特定のインデックスレコードとその直後の「ギャップ(空隙)」を同時にロックする仕組みです。これは「左開右閉区間」で動作し、例えば値10と20の間に存在するすべての挿入や更新を防ぎつつ、20自体も保護します。このロックは主に「REPEATABLE READ」分離レベルで幻影読み取り(Phantom Read)を防ぐために自動的に適用されます。
以下のように、SELECT ... FOR UPDATE を使用するとNext-Key Lockが取得されます:
START TRANSACTION;
SELECT id, name FROM products WHERE price BETWEEN 1000 AND 3000 FOR UPDATE;
-- この範囲内の既存レコードと、その間の挿入ポイントがロックされる
UPDATE products SET stock = stock - 1 WHERE price BETWEEN 1000 AND 3000;
COMMIT;
このロックは共有ロック(S)と排他ロック(X)の両方をサポートします。共有ロックは複数トランザクションでの同時読み取りを許容し、排他ロックは書き込み時の独占アクセスを保証します。
Gap Lock:空の領域を守る予防的ロック
Gap Lockは、実在するレコードではなく、「レコード間の空き領域」をロックします。たとえば、price列に1000と3000のレコードがある場合、その間の1001〜2999の値を挿入しようとする他のトランザクションをブロックします。これにより、範囲検索中に予期せぬ新規レコードの出現(幻影)を防ぎます。
Gap Lockは明示的に指定できませんが、次のような条件で自動的に発動します:
-- トランザクションA
START TRANSACTION;
SELECT * FROM orders WHERE total > 5000 FOR UPDATE;
-- トランザクションB(このINSERTはトランザクションAがCOMMITするまで待機)
INSERT INTO orders (total) VALUES (6000);
注意点として、Gap Lockはパフォーマンスに影響を与える可能性があります。特に大量の範囲ロックが重なると、挿入待ちが増加し、スループットが低下します。必要に応じて「READ COMMITTED」分離レベルへの切り替えも検討すべきです。
Record Lock:個別レコードをピンポイントで固定
Record Lockは最も単純で、特定の1行だけをロックする仕組みです。主キーまたはユニークインデックスを使って明確に1件を特定した場合に使用されます。他のロックと異なり、隣接するギャップはロックしません。
例として、ユーザーIDで明確に1件を操作するケース:
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'U12345' FOR UPDATE;
-- 他のトランザクションはこの行に対してUPDATE/DELETEがブロックされる
UPDATE accounts SET balance = balance - 1000 WHERE user_id = 'U12345';
COMMIT;
Record Lockは、同時実行性を維持しつつ、重要な1件の整合性を保つのに最適です。ただし、WHERE句がインデックスを効率的に使わない場合、意図せずテーブル全体がロックされる可能性があるため、クエリ設計には注意が必要です。
まとめ:ロックの選択は設計の一部
Next-Key、Gap、Recordロックは、それぞれ異なる粒度と目的を持ち、自動的にMySQLが選択します。開発者は、クエリの書き方やトランザクション分離レベルを通じて、これらのロック挙動を間接的に制御できます。過剰なロックは性能劣化を招くため、必要最小限の範囲でロックをかける設計が重要です。