ロック機構の概要
ロックは、複数のプロセスやスレッドがリソースに同時アクセスする際の調整メカニズムです。データベースにおいて、CPU、メモリ、I/Oといった従来のコンピュータリソースに加え、データも多くのユーザーが共有するリソースです。データへの同時アクセスにおける一貫性と有効性を保証することは、あらゆるデータベースが解決しなければならない課題であり、ロック競合はデータベースの並行処理性能に影響する重要な要因です。
OPTIMIZE TABLE文は、削除操作によって生じた断片化された領域を整理し、スペースを回収できます。
ロックの分類
InnoDBのロックは粒度によって、テーブルロックと行ロックに分類できます。
- テーブルロック
- インテンションロック (Intention Lock)
- オートインクリメントロック (AUTO-INC Lock)
- 行ロック
- 共有ロックと排他ロック (Shared and Exclusive Lock)
- レコードロック (Record Lock)
- ギャップロック (Gap Lock)
- レコードロックとギャップロックの組み合わせ (Next-Key Lock)
- 挿入インテンションロック (Insert Intention Lock)
ロックの詳細説明
1. 行ロック
1.1 共有ロック(S)と排他ロック(X)
行の共有ロック(S)と排他ロック(X)は直感的に理解できます。SロックとXロックは互いに競合します。
- 行を読み取る際、他のトランザクションによる変更を防ぐためにSロックを取得します。
- 行を変更する際、他のトランザクションによる同時変更を防ぐためにXロックを取得します。
MySQLはMVCC(Multi-Version Concurrency Control)の特性を持っているため、通常のクエリはロックを取得しない非ロック読み取り(一貫性読み取り)となります。一方、ロックを取得する読み取り(現在の値を読み取る)もあります。
例:
SELECT ... FOR SHARE(MySQL 8.0で追加。以前のバージョンではSELECT ... LOCK IN SHARE MODE): Sロックを取得します。他のトランザクションは読み取り可能ですが、変更はブロックされます。SELECT ... FOR UPDATE: Xロックを取得します。他のトランザクションの変更やSELECT ... FOR SHAREの実行はブロックされます。
1.2 レコードロック
MySQLにおけるレコードロックはすべてインデックス上に設定されます。テーブルにインデックスがなくても、デフォルトで作成されるクラスタ化インデックス上にレコードロックが設定されます。
1.3 ギャップロック
ギャップロックのロック範囲は、インデックスレコード間のギャップ、または最初/最後のインデックスレコードの前後のギャップです。ギャップロックは、トランザクション分離レベルがREPEATABLE READ以上の場合に使用されます。
例えば、あるトランザクションがSELECT * FROM t WHERE c1 > 10 AND c1 < 20 FOR UPDATE;を実行すると、c1=15を挿入しようとする操作はブロックされます。これにより、同じトランザクション内で再びクエリを実行した際の結果が最初と異なる(ファントムリード)ことを防ぎます。
1.4 レコードロックとギャップロックの組み合わせ (Next-Key Lock)
Next-Key Lockは、レコードロックとその前のギャップロックを組み合わせたものです。ギャップロックはREPEATABLE-READレベルでファントムリードの発生を防ぎ、同じトランザクション内での結果の不整合を防止します。SHOW ENGINE INNODB STATUSの出力では、以下のような情報が確認できます。
1.5 挿入インテンションロック
挿入インテンションロックは、INSERT操作のために設定される特別なギャップロックで、主にINSERT操作の並行性能を最適化するために存在します。このロックは挿入の意図を表し、同じインデックスギャップに挿入を行う複数のトランザクションが、挿入する値が異なる場合には互いに待機する必要がなくなります。
例えば、値4と7のインデックスレコードが存在する場合に、値5と6を挿入する2つのトランザクションがあるとします。これらのトランザクションは、挿入行の排他ロックを取得する前に、値4と7の間のギャップに対して挿入インテンションロックを取得しますが、挿入する行が競合しないため、互いにブロックしません。挿入インテンションロック同士は競合しませんが、Next-Key Lockなど他のロックとは競合する可能性がある点に注意が必要です。
行ロックの競合状況は、以下のSQLで確認できます。
SHOW STATUS LIKE 'innodb_row_lock%';
Innodb_row_lock_waitsやInnodb_row_lock_time_avgの値が高い場合、ロック競合が激しいことを示します。
現在のロック情報は以下のSQLで確認できます。
SELECT * FROM information_schema.INNODB_LOCKS \G
InnoDBモニターを有効にして詳細なロック情報を確認することも可能です。
CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;
SHOW ENGINE INNODB STATUS \G
注意: モニター設定後、SHOW ENGINE INNODB STATUSで詳細なロック情報を確認できますが、15秒ごとにログファイルに情報が記録されるため、ログファイルが非常に大きくなる可能性があります。使用後はモニターを無効にすることを推奨します。
2. テーブルロック
MySQLのMyISAMストレージエンジンはテーブルロックのみをサポートしており、これは初期バージョンでサポートされていた唯一のロックタイプです。テーブルロックは今も広く使用されています。
システム上のテーブルロック競合は、table_locks_waitedとtable_locks_immediateステータス変数を分析することで確認できます。
SHOW STATUS LIKE 'table%';
Table_locks_waitedの値が大きい場合、テーブルロックの競合が激しいことを示します。
テーブルロックのモード
MySQLのテーブルロックには2つのモードがあります:テーブル共有ロックとテーブル排他ロックです。
- MyISAMでのテーブル読み取り操作は、他のユーザーによる同じテーブルの読み取りリクエストをブロックしませんが、同じテーブルへの書き込みリクエストをブロックします。
- MyISAMでのテーブル書き込み操作は、他のユーザーによる同じテーブルの読み取りおよび書き込み操作をブロックします。
したがって、スレッドがテーブルの書き込みロックを取得すると、ロックを保持するスレッドのみがテーブルを更新でき、他のスレッドの読み書き操作はロックが解放されるまで待機します。
ロックの取得
通常、レコードのクエリや変更時には、MySQLが自動的に読み取りロックまたは書き込みロックを取得します。
手動でロックを取得する場合:
LOCK TABLES table1 READ LOCAL, table2 READ LOCAL;
LOCK TABLE table1 WRITE;
-- 処理実行後
UNLOCK TABLES;
2.1 テーブルロック - インテンションロック
インテンションロックはMySQLではテーブルレベルロックであり、今後テーブルにどのタイプのロック(IS/IX)を追加するかを示します。
SELECT ... FOR SHAREは、インテンション共有ロック(IS)を取得します。SELECT ... FOR UPDATEは、インテンション排他ロック(IX)を取得します。
テーブル内の行の共有ロックを取得する前に、まずテーブルのISロックを取得する必要があります。テーブル内の行の排他ロックを取得する前に、まずテーブルのIXロックを取得する必要があります。インテンションロックと行ロックの競合および互換性は、専用のマトリックスで確認できます。
インテンションロックは、テーブルロックリクエスト(例:LOCK TABLE ... WRITE文の実行)以外のロックを妨げません。これは次のように理解できます:テーブルロックを申請する際(LOCK TABLE文実行時)、テーブルレコードにロックが存在してはなりません。インテンションロックがない場合、すべてのレコードをスキャンしてロックの存在を確認する必要があります。しかし、インテンションロックがあれば、テーブルにインテンションロックが存在するかどうかを判断するだけで済みます。インテンションロックが存在する場合、テーブル内の何らかのレコードがロックされているか、ロックされる予定であることを意味し、テーブルロック申請文(LOCK TABLE)は待機します。インテンションロックの設計は効率を向上させます。
2.2 テーブルロック - オートインクリメントロック
オートインクリメントロックは、AUTO_INCREMENTフィールドを持つテーブルに挿入を行うトランザクションが使用する特別なテーブルレベルロックです。
最も単純なケースでは、あるトランザクションがテーブルに値を挿入している間、他のすべてのトランザクションは挿入文の実行が完了するまで待機する必要があります。これにより、後続のトランザクションが挿入する主キー値が連続していることが保証されます。
innodb_autoinc_lock_modeパラメータは、オートインクリメントロックのアルゴリズムを制御し、自動増分値の生成戦略を制御することで並行性能を向上させます。
ロックモードの意味
SHOW ENGINE INNODB STATUSを使用してロック情報を確認する際、よくLOCK_MODEフィールドを目にします。これはロックモードを表し、各種モードがそれぞれ何を意味するのか理解することで、ロック待機やデッドロックの問題をより効果的に分析できます。これは非常に重要な知識点です。
- IX: インテンション排他ロックを表します。
- X: Next-Key Lockがレコード自体とその前のギャップをロックしていることを表します(X)。
- S: Next-Key Lockがレコード自体とその前のギャップをロックしていることを表します(S)。
- X, REC_NOT_GAP: レコード自体のみをロックしていることを表します(X)。
- S, REC_NOT_GAP: レコード自体のみをロックしていることを表します(S)。
- X, GAP: ギャップロックを表し、レコード自体はロックしません(X)。
- S, GAP: ギャップロックを表し、レコード自体はロックしません(S)。
- X, GAP, INSERT_INTENTION: 挿入インテンションロックを表します。
他のデータベースと比較して、MySQLのロック機構は比較的シンプルです。最も顕著な特徴は、異なるストレージエンジンが異なるロック機構をサポートしている点です。例えば、MyISAMおよびMEMORYストレージエンジンはテーブルレベルロックを採用し、BDBストレージエンジンはページロックを採用していますが、テーブルレベルロックもサポートします。InnoDBはデフォルトで行レベルロックを使用しますが、テーブルレベルロックもサポートします。
MySQLの3つの主要なロックの特性は以下のようにまとめられます。
- テーブルレベルロック: オーバーヘッドが小さく、ロックが高速で、デッドロックが発生せず、ロック粒度が大きい。ただし、ロック競合の確率が最も高く、並行性が最も低い。
- 行レベルロック: オーバーヘッドが大きく、ロックが低速で、デッドロックが発生する可能性があり、ロック粒度が最小。ただし、ロック競合の確率が最も低く、並行性が最も高い。
- ページロック: オーバーヘッドとロック速度はテーブルロックと行ロックの中間。デッドロックが発生する可能性があり、ロック粒度と並行性も中間。
このことから、テーブルロックは主にクエリが中心で、インデックス条件に基づく少量のデータ更新を行うアプリケーション(Webアプリケーションなど)に適しています。行ロックは、インデックス条件に基づく大量の同時更新が少量の異なるデータに対して行われ、同時に同時クエリもあるアプリケーション(オンラインOLTPシステムなど)により適しています。
MySQL 8.0以降では、performance_schema.data_locksテーブルを通じてデッドロック情報を確認できます。