月別アーカイブ: 2013年2月

MySQLの行ロックのふしぎ挙動で夜も安心して眠れない

MySQLのびみょーな行ロックに悩まされたのでメモ代わりに。
全部MySQL5.5でInnoDB使っている時のお話デス。

こんなテーブルが有るとしますよ。

create table table001 (
  id int primary key,
  name text
);

そしてこんなデータが入っています。

A> select * from table001;
+----+-----------------+
| id | name            |
+----+-----------------+
|  1 | 水瀬伊織        |
|  2 | 伊織さま        |
|  3 | いおりん        |
|  4 | デコちゃん      |
+----+-----------------+

まあ、データの中身は気にしない方向で。

そんなアイマス好きのAさん。
伊織さまを「デコちゃん」呼ばわりしている id=4 が許せないので書き換えてやろうと決意し、 id=4 に対して select ~ for update で行ロックを取得すべく以下のようなSQLを実行します。

A> begin;
A> select * from table001 where id = 4 for update;
+----+-----------------+
| id | name            |
+----+-----------------+
|  4 | デコちゃん      |
+----+-----------------+

そんな作業中、別の人(プロンプトBね)が何かしようとしていますよ。

B> begin;
B> select * from table001 where name = 'いおりんビーム' for update;

はい、なぜかBさんはここでロックかかってしまいました。
え、name=’いおりんビーム’ なんて行は存在しないのですぐに返ってきて欲しいのですが、なぜ?

ところでAさん、伊織さまの悪口修正作業が終わったみたいですね。

A> update table001 set name = 'デコ呼ばわりとは何事か、このビッチめ' where id = 4;
A> select * from table001 where id = 4 for update;
+----+--------------------------------------------------------+
| id | name                                                   |
+----+--------------------------------------------------------+
|  4 | デコ呼ばわりとは何事か、このビッチめ                   |
+----+--------------------------------------------------------+
A> commit;

ふう、無事に悪口を消してやったぜ。
なんということでしょう、Aさんがコミットした瞬間にBさんのロックが解かれたのです!

えええええええええ、なんでAさんの行ロックがBさんを止めるわけ?????

そしてAさん、id=5を追加しようとしました。

A>  insert into table001 values(5, '水瀬伊織さま');

これもロックに引っかかる!

は? これテーブルロックかかってね?

そう、実にMySQLの行ロック周りはクセのある動きをしやがるのです。
この場合、Bさんの打ったSQLがポイント。
再度提示しましょう。

B> select * from table001 where name = 'いおりんビーム' for update;

ポイントはカラムname。こいつは主キーでもユニークキーでもない、どんだけ重複しようがnullが入ろうが自由だ! なカラムです。
MySQLの select ~ for update は、一意に定まらないカラムに対して検索かけるとテーブルロックをかけようと振舞いやがります。
なので、まずBさんのselectがAさんのロックとかち合ってすっ止まりやがり、そしてAさんがコミットしたら、今度はBさんのテーブルロックがかかるのでAさんのinsertがすっとまる。

えええええええええ、なにこれ?????

逆に一意に定まるカラム(この場合はidの方)は、存在しなくてもテーブルロックかけたりしません。
さっきの例だとBさんは以下のSQLなら何事も無く動いたでしょう。

B> select * from table001 where id = 100 for update;

id=100なんて存在しない主キーのためロックもかかりません。
当然だよね、存在しないんだから。

あーーーーーーーーめんどくせーーーーーーーー。

などと言っていても解決しないので、MySQLでは select ~ for update で行ロックを取りたい時は必ず一意に決まるカラムで検索しましょう。主キーとかユニークキーとかでね。

となると、例えば検索条件が主キー+制約なしのカラムだとどうなるか、試してみたくなりますね。
そこでこんなテーブルを作りますた。

create table table002 (
  id int primary key,
  name text,
  nazo int
);

このテーブルにはこんなデータが入ってます。

A> select * from table002;
+----+-----------------+------+
| id | name            | nazo |
+----+-----------------+------+
|  1 | 三浦あずさ      |   91 |
|  2 | 如月千早        |   72 |
+----+-----------------+------+

くっ…
さて、さっそく主キー+制約なしカラムで行ロックを取得しましょう。

A> begin;
A> select * from table002 where id = 1 and nazo > 90 for update;
+----+-----------------+------+
| id | name            | nazo |
+----+-----------------+------+
|  1 | 三浦あずさ      |   91 |
+----+-----------------+------+

そしてBさんもなにかやってみます。

B> select * from table002 where id = 1 for update;
→Aさんのロックに引っかかる。

B> select * from table002 where id = 2 for update;
→問題なく取得成功。

B> select * from table002 where id = 1 and nazo < 90 for update;
→Aさんのロックに引っかかる(!!)

B> select * from table002 where id = 2 and nazo > 90 for update;
→問題なく取得成功。該当データなし。

B> select * from table002 where id = 3 for update;
→問題なく取得成功。該当データなし。

B> select * from table002 where id = 3 and nazo > 90 for update;
→問題なく取得成功。該当データなし。

うーん、なんともしっくり来ませんが、これがMySQLの振る舞いでやがりますよ。

なので、よくある「存在すればupdate、存在しなければselect」なんてやる時に、必ず検索条件が主キーかユニークキーになっていなければ、なんと恐ろしいことになるのやら。
夜も安心して眠れないのでありますですよ…