MySQLのロックについて調べてみた

アプリケーション開発をしていると頻繁には遭遇しないもののデッドロックなどに遭遇することがあります。
今回デッドロックについて改めて調べてみようと思ったのですが、その前にロックについて理解しておこうと思い調べてみました。
MySQL(InnoDB)におけるロックとは何なのかMySQLのリファレンスをベースに学習した内容の整理のためにもまとめてみようと思います。

https://dev.mysql.com/doc/refman/8.4/en/innodb-storage-engine.html

## ロック(Locks)とは
ロックとは英語の"lock"であり錠🔐のことです。
データベースへのレコードの取得、更新などを行う際にそのレコードがあるトランザクションから操作されていることを表すもので、他のトランザクションが同時にそのレコードを更新してしまわないようにして、データの不整合を防ぐための仕組みです。

## 内部ロック方法
知っておく必要のあるロックのレベルに「行レベルロック」と「テーブルレベルロック」という2つがある。
これらは内部ロック方法(Internal Locking Methods)と呼ばれます。

### テーブルレベルロック
テーブルレベルロックはテーブル全体を範囲としてロックするものです。
テーブル全体やテーブルの大部分へのアクセスを必要とするようなケースでは高速かつ少ないメモリのみ必要とするという利点があるが、テーブル全体をロックしてしまうためその間他のトランザクションが待ち状態になってしまう欠点もあります。

### 行レベルロック
行レベルのロックはテーブル全体ではなく、テーブルの中の行のレベルでロックするものです。
行レベルでのロックになるため、複数のトランザクションが同じテーブルにアクセスすることができます。

## ロック
### ロックの強さ
まずロックには「共有ロック」と「排他ロック」というロックの強さの違う概念があります。

#### 共有ロック(Shared Locks)
行レベルロックの1つです。
複数のトランザクションが同じ行に対して保持することができるロックです。
ただし読み取りのみ可能なロックで、書き込みを行うことはできません。
共有ロックがある行に対して同時にこの下で説明する排他ロックを取得することはできません。

#### 排他ロック(Exclusive Locks)
行レベルロックの1つです。
複数のトランザクションが同じ行に対して保持することができないロックです。読み書き可能なロックです。
そのため共有ロックのように複数のトランザクションが同じ行に対して排他ロックを持っているという状況は起きないということです。

### ロックの種類
#### インテンションロック(Intention Locks)
テーブルレベルでのロックです。
インテンションロックには「インテンション共有ロック」と「インテンション排他ロック」の2つのロックが存在します。
このインテンションロックというロックが存在する目的は、あるテーブルの行の中にロックが取得されているものは現在あるのか、もしくはこの後取得される予定のものはあるのかを示すためにあります。
これがない場合にはあるトランザクションがテーブル内のある行にロックを保持している時、テーブル全体への操作を行う前にテーブルの各行のロックを確認していかなければならなくなるため非常に大変です。そのような問題を解決するためそのテーブルの中の行でロックが取得されているものがあるのかを示すために必要なのがこのインテンションロックです。

インテンションロックは下記のような競合関係にあると説明されています。

×は競合するもの、○は競合しないものです。

|  | 排他ロック | インテンション排他ロック | 共有ロック | インテンション共有ロック |
| ---- | ---- | ---- | ---- | ---- |
| 排他ロック | × | × | × | × |
| インテンション排他ロック | × | ○ | × | ○ |
| 共有ロック | × | × | ○ | ○ |
| インテンション共有ロック | × | ○ | ○ | ○ |

#### どう取得されるの？
MySQLのリファレンスの中には
> For example, SELECT ... FOR SHARE sets an IS lock, and SELECT ... FOR UPDATE sets an IX lock.
> The intention locking protocol is as follows:
> - Before a transaction can acquire a shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.
> - Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.

と記述があります。
つまり共有ロック、排他ロックを取得する際にその行を持つテーブルに対してロックを取得するものだと考えられます。

#### レコードロック(Record Locks)
レコードロックは、プライマリキー、ユニークキーを利用してインデックスレコード上で行をロックするやり方です。

#### ギャップロック(Gap Locks)
ギャップロックはインデックスレコードの間に取得されるロックのことです。また最初のインデックスレコードの前、もしくは最後のインデックスレコードの後に取得される場合もギャップロックです。
つまりは特定のレコードに対してロックを取得するのではなく、インデックスレコードのある範囲に対して取得されるのがギャップロックです。
ギャップロックは、単一のインデックスに対して発生する場合もあれば、複数のインデックスに対して発生する場合もあれば、さらには空、つまりテーブルに存在しない値の範囲に対して発生する場合もあります。

ユニークインデックスを使って特定のレコードを検索する場合にはギャップロックは必要ありません。
ただし、例えば`column_a`、`column_b`の2つのカラムで複合ユニークインデックスである時に、`column_a`のみで検索した場合にはギャップロックが発生しますので注意です。

#### ネクストキーロック(Next-Key Locks)
特定行とその行の前のギャップをロックするものです。
レコードロックとギャップロックの組み合わせのロックです。

### インサートインテンションロック(Insert Intention Locks)
インサート操作によって取得されるギャップロックです。
インサートを行う前にまずインサートする位置にギャップロックを取得するということですね。
複数のトランザクションが同じインデックスギャップに対して行をインサートを行おうとしていても、そのギャップの中の同じ位置にインサートしなければお互いに待ちは発生しないという特徴があります。

### AUTO-INCロック(AUTO-INC Locks)
オートインクリメントが設定されているカラムを含むレコードをインサートする場合に発生するロックです。
複数のトランザクションでオートインクリメントが設定されているカラムを含むレコードをインサートしようとしている場合には、先にロックを取得しているインサート操作の完了を待たなければいけないために発生するロックです。

## まとめ
今回ロックについて調べてみましたが、まだまだ概念レベルでここからさらに知識のアップデートと詳細の理解が必要だなと感じました。
今回より詳細への学習に向けて良い機会となりました。この辺りの知識習得に良さそうな書籍等ご存知の方おりましたらコメント等いただけますと幸いです！

## 参考とさせてもらったページ
- https://dev.mysql.com/doc/refman/8.4/en/innodb-storage-engine.html
- https://speakerdeck.com/yoku0825/mysqlnorotukunozhong-lei-tosonojing-he
- https://dev.to/eyo000000/a-straightforward-guide-for-mysql-locks-56i1
- https://speakerdeck.com/andpad/mysqlfalserotukunituite-ji-ben-bian-d367fac2-7653-4e19-b811-2c326703d326
- https://bmf-tech.com/posts/MySQL%E3%81%AE%E3%83%AD%E3%83%83%E3%82%AF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6#2-%E3%83%AD%E3%83%83%E3%82%AF
- https://zenn.dev/gibjapan/articles/1d8dfb7520dabc