There's an echo in my head

日々のメモ。

MySQL InnoDBの行ロック

ロックがわからない。MySQL InnoDBの行レベルロックを読んだけど、イマイチわからない。というわけで、社内の勉強会で知ったことをまとめてみる。

FOR UPDATEかLOCK IN SHARE MODEによって、そのトランザクションに走る別画面でのクエリの処理のタイミングが異なる。

FOR UPDATEによるロック

まずは画面Aで接続が行われ、トランザクションが開始され、SELECT文が発行されたとする。

[画面A]

BEGIN; // トランザクションA開始
SELECT * FROM users WHERE id = 1 FOR UPDATE;
COMMIT; // トランザクションA終了

そしてトランザクションAの開始後〜終了前に画面B〜Dがクエリを発行したとする。

ちなみにトランザクションを開始しておかないとFOR UPDATEを書いても画面Aでのロックが始まらないので要注意。

[画面B]

// トランザクションAでSELECT発行後にクエリ発行
SELECT * FROM user_poarams WHERE id = 1 FOR UPDATE;
// … トランザクションAのCOMMITが終わるまで返ってこない

[画面C]

// トランザクションAでSELECT発行後にクエリ発行
SELECT * FROM user_poarams WHERE id = 1 LOCK IN SHARE MODE;
// … トランザクションAのCOMMITが終わるまで返ってこない

[画面D]

// トランザクションAでSELECT発行後にクエリ発行
SELECT * FROM user_poarams WHERE id = 1;
// トランザクションAのCOMMITを待たずに即座に返ってくる

LOCK IN SHARE MODEによるロック

まずは画面Eで接続が行われ、トランザクションが開始され、SELECT文が発行されたとする。

[画面E]

BEGIN; // トランザクションE開始
SELECT * FROM users WHERE id = 1 FOR UPDATE;
COMMIT; // トランザクションE終了

そしてトランザクションEの開始後〜終了前に画面F〜Hがクエリを発行したとする。

[画面F]

// トランザクションEのSELECT発行後にクエリ発行
SELECT * FROM users WHERE id = 1 FOR UPDATE;
// … トランザクションEのCOMMITが終わるまで返ってこない

[画面G]

// トランザクションEのSELECT発行後にクエリ発行
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
// トランザクションEのCOMMITを待たずに即座に返ってくる

[画面H]

// トランザクションEのSELECT発行後にクエリ発行
SELECT * FROM users WHERE id = 1;
// トランザクションEのCOMMITを待たずに即座に返ってくる

ひとまずまとめ

先に接続した方のロック方法 FOR UPDATE LOCK IN SHARE MODE ロック無し
(A) FOR UPDATE (B) 待つ (C) 待つ (D) 待たない
(E) LOCK IN SHARE MODE (F) 待つ (G) 待たない (H) 待たない

フィーリングで理解してみる

FOR UPDATEでロックをすると、ロック無しの場合を覗いてそのトランザクションが終わるまでレコードの読み取りを待たせる。つまり自分(A)以降の他のクエリ(B)(C)に対して自分のトランザクションで行われた変更を必ず反映させる。逆に、自分(B)(F)より前にロックしているクエリ(A)(E)がいたら、そのトランザクションが終わるまで待つ。つまり、前後のクエリに対して自分の処理が排他的に行われるため、最新の情報を扱うことができる。

すべてFOR UPDATEにすれば必ず一貫性は保たれるけど、トランザクションが終わるまで待つために 処理がなかなか進まない。読み取るだけだから同時に走ってもいいよ、トランザクションも必要ないよってこともある。なのでFOR UPDATEだけでは不十分。

一方、LOCK IN SHARE MODEでロックすると、FOR UPDATEによるロック以外では待たない。比較的カジュアルに取りに行く。でもFOR UPDATEをかけた場合(A)(E)にはしっかり待つ。なので、FOR UPDATEほど排他的ではなく流れもスムーズだし、FOR UPDATEによる変更を待つので最新の情報を扱うことができる。

じゃあデフォルトでLOCK IN SHARE MODEによるロックを掛けて、明示的にFOR UPDATEを宣言したときだけ排他ロックかければいいんじゃないかと。ただこうなっていない理由は、完全に想像だけど、読み出しのみのマスターデータだったらロックなんて要らないし、むしろリードの回数が多すぎてロックしてる暇なんてないよってことがあるからじゃないかなぁ。想像。

そんなわけで、

  • 「更新するから整合性のために待ってて!俺も待つし!」ってときにはFOR UPDATE
  • 「自分は更新しないけど、他の人がFOR_UPDATEで更新するなら待つよ!」ってときにはLOCK IN SHARE MODE
  • 「更新することもされることもないからガシガシ読みに行くよ!」ってときにはロック無し

という理解をしたんだけど、どうなんだろう。

追記

わかりやすい記事を見つけたのでメモしておく。

MySQLのINSERT/UPDATE時におこる不整合対策

このブログに出てくるコードスニペッツは、引用あるいは断りがない限りMITライセンスです。