良く分かるMySQL Innodbのギャップロック


MySQLのロック

ロックとはトランザクションの並列度を上げる為の並列スケジューリング方法の一つ

トランザクションをサポートしているデータベースにおいては、トランザクションの並列数を上げる事が性能アップの一つの方法。
他のトランザクションに更新して欲しくないデータだけにロックをかけて、ロックされたデータ以外を更新するトランザクションは並列で実行される。

Innodbは行ロック?

Innodbは更新対象の行だけをロックする。と思っていると、意外な落とし穴にハマる。
その一つがギャップロック。

ギャップロックを実際に起こしてみる

サンプルテーブル

idとstrがあるだけのシンプルなテーブル。idがPKで1~5までは順番に、その後、10,20と飛んで行が入っている。

通常の行ロック

トランザクション1

select for updateでid=2の行を明示的にロック

トランザクション2

id=1,3の更新は可能、id=2の更新は待たされる(画像分かりづらいが、ロックにより待たされているので応答が返ってこない)。

これは通常の行ロックで、トランザクション1で触った行だけがロックされている。

ギャップロック

トランザクション1

idが飛んでいる範囲の5~10をselect for updateしてみる。
行ロックの感覚でいると、selectされてきたid=5と10の行にだけロックがかかると思うが、、、

トランザクション2

id = 7に新しくinsertしてみると応答が返ってこない(ロックで待たされる)。

もともと存在しているのはid=5と10だけなので、その2行がロックされると思いきや、存在していないid=7にinsertしようとするとそこにもロックがかけられている。これがギャップロック。

selectが空ぶってもギャップロックがかかる

トランザクション1

id = 6~9の範囲でselect for updateをする。つまり、どの行も実際には触っていない。

トランザクション2

そんな場合でもid = 7にinsertは出来ない。

一件どの行もロックする必要がなさそうに見えるselectの空振り時にもギャップロックがかかってしまう。

範囲検索じゃなくても空ぶったらギャップロック

トランザクション1

トランザクション2


範囲だろうと、id指定だろうと空振りでロックがかかる

範囲検索外でもギャップに入ってたらロックがかかる

トランザクション1

id > 9 でselect for update

トランザクション2

id = 7は一件select for update外にいるがギャップロックのせいでロックがかかる

要するにギャップロックとは

インデックスとインデックスの間にかけられるロック。
インデックス単位でのロックを考えようとするとわりと分かりやすい仕組みかなと思う。
ロックの対象範囲を最小限にしつつ、トランザクション的に異常が起きないようにしようとすると確かにそうなる。
MySQL的には、5と10の間に6,7,8,9って入るなーなんて考えないので。
ちなみに、プライマリキーも何も無いテーブルで同じ事をするとテーブルロックがかかる。これもインデックス単位でロック範囲を絞り込みしている事を考えると自然。
要するにどこの範囲に更新されると不都合か、という事が全く分からないのでテーブル全体をロックするということ。

実際に起きる事故とか

例えば、100人ユーザーがいるサービスのユーザーテーブルで間違えて(バグで)id = 200に対してロックをかけてしまった場合、この場合のギャップはid > 100となるのでこのタイミングで新規入会してきたユーザーはロック待ちになる。ロックが解除されなければタイムアウトなどで会員登録が出来無いようなエラーになる可能性すらある。