SELECT ... FOR UPDATEとロックの挙動
kamipoさんが補足を書いてくれたので、参照するとよいです。
基礎的だけど、SELECT ... FOR UPDATEをちゃんと理解できてない気がするな、ということで実際にコンソールで打ちながら挙動を確認してみた。
今回確認した環境は、
mysql> show variables like 'tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec) mysql> show variables like 'version'; +---------------+--------+ | Variable_name | Value | +---------------+--------+ | version | 5.6.12 | +---------------+--------+ 1 row in set (0.00 sec)
今回利用するテーブルはこんな感じ
> CREATE TABLE `select_for_update` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `col1` int(10) unsigned NOT NULL, `col2` int(10) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `col1` (`col1`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; > insert into select_for_update (col1) values (1),(2),(3),(4),(5),(6),(7),(8),(9);
主キー指定でロックした場合
client1> begin; client1> select * from select_for_update where id = 1 for update;
この状態で、別のクライアントから、
client2> select * from select_for_update where id = 1;
とすると、書き込みロックなので、読み取りは何も問題なく行なえる。
まず主キーのときの挙動
client2> select * from select_for_update where id = 1 for update;
とすると、ロック待ちになる。この状態でclient1でcommitすると、client2も実行結果が返ってくる。
client2> update select_for_update set col1 = 1 where id = 1;
とすると、これも更新のクエリなのでロック待ちになり、client1がcommitするまで返ってこない。
ここまでは、まぁ当然の挙動。
client2> select * from select_for_update where id < 3 order by id desc limit 1 for update
はブロックされない。
client2> update select_for_update set col1 = 2 where id < 3 order by id desc limit 1
はブロックされない。
次に、セカンダリインデックスのときの挙動。
client2> update select_for_update set col1 = 2 where col1 < 3 order by id desc limit 1 client2> select * from select_for_update FORCE INDEX (col1) where col1 < 3 order by id desc limit 1 for update
はusing indexだが、using filesortになるため行の読みこみが発生し、ブロックされる。
client2> select * from select_for_update FORCE INDEX (primary) where col1 < 3 order by id desc limit 1 for update
primary keyをINDEXとして使うようにすると、ブロックされないようだ。これは、5.6のtop sort最適化のためっぽい?
client2> select * from select_for_update force index (col1) where col1 < 3 order by col1 desc limit 1 for update client2> select * from select_for_update force index (col1) where col1 < 3 order by col1 desc, id desc limit 1 for update;
はcol1が使われ、using indexのみでブロックされない。
rangeでロックした場合
client1> begin; client1> select * from select_for_update where id < 2 for update
client2> select * from select_for_update where id = 2 for update
はid=1の行のみがブロックされると期待してしまうが、実際にはid=2の行もロックされているため、ブロックされてしまう。これはInnoDBのネクストキーロック問題と呼ばれているそうな。
client2> select * from select_for_update where id = 3 for update
はブロックされない。
では、主キーではなく、セカンダリインデックスを使うとどうなるか、
client2> select * from select_for_update where col1 = 2 for update
これもブロックされるようだ。
client2> select * from select_for_update where id < 3 order by id desc limit 1 for update
など、結果がid=2になるようなクエリはブロックされる。
select * from select_for_update where id < 4 order by id desc limit 1 for update
はブロックされない。
他の結果については、id=3になるように調整して打った結果、同様の結果になった。
client1> begin; client1> select * from select_for_update FORCE INDEX (primary) where col1 < 2 order by id desc limit 1 for update
でロックしてみる。
select * from select_for_update FORCE INDEX (primary) where col1 < 3 order by id desc limit 1 for update select * from select_for_update FORCE INDEX (primary) where col1 < 4 order by id desc limit 1 for update select * from select_for_update FORCE INDEX (col1) where col1 < 3 order by id desc limit 1 for update select * from select_for_update FORCE INDEX (col1) where col1 < 4 order by id desc limit 1 for update
さきほどはうまくいった、force index (primary)が、今度はブロックされてしまう。
client1> begin; client1> select * from select_for_update FORCE INDEX (col1) where col1 < 2 order by id desc limit 1 for update;
であれば、
client2> select * from select_for_update FORCE INDEX (primary) where col1 < 3 order by id desc limit 1 for update
はブロックされず、
client2> select * from select_for_update FORCE INDEX (col1) where col1 < 3 order by id desc limit 1 for update
はブロックされる。
いまいち何故こうなるのかはまだ理解できてない。
追記:
@kamipo: @walf443 触れた行(というかインデックス)をロックするから、最後のprimary keyのやつはid=9からcol1<2にマッチする行が1つ見つかるまで(id=1まで)のインデックスをロックするんで今回利用してるテーブルの場合は全てのレコード(のギャップ)をロックしてます
2013-08-30 01:52:52 via YoruFukurou to @walf443
@kamipo: @walf443 セカンダリインデックスの例のほうはキープレフィックスcol1<2の範囲のインデックスがロックされるから、その範囲に触れないクエリはブロックしない。最後から2つめのクエリがブロックしないのはprimary key上では触れる範囲が被ってないから。
2013-08-30 02:10:49 via YoruFukurou to @walf443
とのことですが、まだ詳しくは確認できてないので、あとで別エントリで補足します。