↓↓↓↓↓を参考にInnoDBの共有ロックと排他ロックとインテンションロックについてまとめました。
共有ロックと排他ロック
InnoDBでは共有ロック(S)と排他ロック(X)という2つのレコード(行)レベルロックが実装されている。
ロック | 説明 |
---|---|
共有ロック(S) | 別のトランザクションの読み取りを許可するが、書き込みは許可しない。 複数のトランザクションが同じ行に対して共有ロックを保持することができる。 |
排他ロック(X) | 別のトランザクションによる読み取り、書き込み両方を許可しない。 ただし、InnoDB 分離レベル、REPEATABLE READ の場合は読み取りできる。 |
トランザクションT1が行rに対する共有ロックを保持しているとき、別のトランザクションT2から行rに対するロック要求は次のようになる。
T1 | T2 | 結果 |
---|---|---|
共有ロック(S) | 共有ロック(S) | T2による共有ロックのリクエストは、すぐに許可(付与)。 |
共有ロック(S) | 排他ロック(X) | T2による排他ロックのリクエストは、すぐには許可(付与)されない。 待たされる。 |
トランザクションT1が行rに対して排他ロックを保持しているときは、
T2から行rに対する共有ロック・排他ロックどちらのリクエストもすぐには許可(付与)されず、T1によるロックが開放されるまで待たされる。
インテンションロック(InnoDBのテーブルロック)
インテンションロックは、テーブルレベルに適用するタイプのロック。
トランザクションがテーブル内の行に対して、どんなタイプ(共有/排他)のロックを獲得しようとしているかを示す。
インテンションロックには、次の2つある。
ロック | 説明 |
---|---|
インテンション共有ロック(IS) | トランザクションはテーブル内の各行に共有ロックを設定する。SELECTなどでテーブルから行を参照するとき。SELECT ~ LOCK IN SHARE MODE; |
インテンション排他ロック(IX) | トランザクションTはテーブル内の各行に排他ロックを設定する。 INSERT/UPDATE/DELETEなどの更新系クエリによってテーブルの行に変更を加えるとき。 SELECT ~ FOR UPDATE |
インテンションロックと共有・排他ロック
ある行を共有ロックまたは排他ロックする前に、必ずインテンションロックを獲得する仕組みになっている。
-
行の共有ロックを獲得するために、テーブルのインテンション共有ロックを獲得
-
行の排他ロックを獲得するために、テーブルのインテンション排他ロックを獲得
そして、インテンションロック(IS・IX)と共有ロック(S)、排他ロック(X)の競合関係は次の表のようになる。
X | IX | S | IS | |
---|---|---|---|---|
X | × | × | × | × |
IX | × | ○ | × | ○ |
S | × | × | ○ | ○ |
IS | × | ○ | ○ | ○ |
○: 競合しない、×: 競合する
- 既存のロックと競合しない場合は、ロックが付与される。
- 競合している場合は、ロックが付与されない。(競合している既存のロックが解放されるまで待機)
トランザクションは、競合している既存のロックが解放されるまで待機する。既存のロックと競合し、デッドロックが発生する場合は、エラーが発生する。
この表をよく見るとわかるように、インテンションロック(IS・IX)同士は競合しないため、InnoDBの排他制御は行ロックとなることがわかる。
ロック待ちを起してみる
次のようなactorテーブルがあるとします。
actor_id | first_name | last_name | last_update |
10 | CHRISTIAN | GABLE | 2006-02-15 04:34:33 |
このテーブルに次の順でクエリを実行してみます。
- トランザクション1で
SELECT * FROM actor WHERE actor_id = 10 LOCK IN SHARE MODE;
actorテーブルにISロック、レコードにSロックをかける - トランザクション2で
UPDATE actor SET first_name = 'MIKE' WHERE actor_id = 10;
actorテーブルにIXロック、レコードにXロックを要求
順を追って実行結果を見ていきます。
トランザクション1でISロック、Sロックを獲得
SELECT * FROM actor WHERE actor_id = 10 LOCK IN SHARE MODE;
を実行。
ロック状態を確認。
テーブルにISロック、レコードにSロックがかかっています。
トランザクション2でIXロック、Xロックを要求
この状態で別のトランザクションからUPDATE文を実行します。
実行するとロック獲得待ちで結果が帰ってきません。
しばらく放置していると、Lock wait timeout exceeded;
が表示されます。
このときのロックの状態を確認すると、
SELECT ENGINE, OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA FROM performance_schema.data_locks;
IXロックは獲得(GRANTED)しているが、Xロックは獲得できいない(WAITING)
innodb statusでも確認してみます。
show engine innodb status\G
こちらでもロックの獲得待ちが発生しているのがわかります。
デッドロック
デッドロックとは、トランザクション同士のロックが競合することで、互いに待ち状態になり、どちらのトランザクション処理も進まなくなること。
実際にデッドロックを発生させてみる。例として支払情報を管理するpaymentテーブルがあるとする。
1. まず、トランザクション1でpaymentテーブルにインテンション共有ロックを、対象のレコードに共有ロックをかける。
トランザクションがテーブルのある行の共有ロックを獲得するには、まずテーブルのインテンション共有ロックまたはそれより強いロックを獲得する必要がある。
2. 次に別のトランザクション2から同じレコードを削除しようとする。
削除を行うには、排他ロックが必要だが、トランザクション1が保持している共有ロックと競合するため、ロックが付与されない。そのため、トランザクション2はブロックされ、待機状態になる。(クエリの結果も返ってこない)
3. トランザクション1からも同じレコードを削除しようとする
トランザクション2はすでに排他ロックのリクエストをしていて、トランザクション1が共有ロックを解放するまで待機している。そのため、トランザクション1は行を削除するために排他ロックが必要だが、獲得できないためにデッドロックが発生する。
その結果、InnoDBはトランザクションのどちらかに対してエラーを生成し、そのロックを解放し、次のエラーを返す。
このエラーが発生した時点で、ロックリクエストを付与できるようになり、テーブルから行が削除される。