インフィニットループ 技術ブログ

2024年08月08日 (木)

著者 : nob

MySQL/Aurora/TiDBロック入門 – 第10回ロック読取りは READ COMMITTED【解説動画付】

MySQL と互換 DB のロック入門、MySQL 編の最終回となる第10回の今回は、ロック読取りが READ COMMITTED と似た挙動となる件について解説します。

MySQL に限らず DB を使ってアプリケーションを作成する場合、「自分の処理が終わるまでは他のトランザクションにはデータを変更してほしくない」という状況が発生します。これを実現するには 第1回の記事 で解説したトランザクション分離レベルだけでは不十分で、必要な領域に共有や排他の指示が付いた SELECT によってロックをかける「ロック読取り」が必要となります。

MySQL のデフォルトは REPEATABLE READ の分離レベルで

ー ロック読取りや更新を範囲に対して指定すると、ギャップロックを含んだネクストキーロック
ー 対象の行がある場合は、READ COMMITTED で使われているのと同じ単純なレコードロック
ー 対象行が無い場合は、隙間へのギャップロックで他のトランザクションからの挿入をブロック

します。実戦ではこうしたロック読取りが必須となりますが、ロック読取りは分離レベルとの関係で重要なポイントがありますので解説します。

第1回 トランザクション分離レベル
第2回 ロックモニターの読み方
第3回 ロック読取りも SELECT は止められない
第4回 INSERT を止めるインテンションロック
第5回 WHERE 条件と違うロック読取り
第6回 performance_schema
第7回ギャップロックと消えるロック
第8回 ネクストキーロックと降順インデックス
第9回 共有ロックでデッドロック
第10回 ロック読み取りは READ COMMITTED
第11回 Aurora と MySQL の違い
第12回 TiDB と MySQL の違い

第10回ロック読取りは READ COMMITTED 解説動画

ロック読取りがファジーリードになる

READ COMMITTED において他のトランザクションでコミット済みの最新値がトランザクションの途中で見えてしまう現象があります。これはノンリピータブルリードまたはファジーリードと呼ばれます。第1回 で解説したように、MySQL デフォルトの REPEATABLE READ はこれを防いでくれます。

以下の t1 テーブルでトランザクションを開始し全行を SELECT します。

[1]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[1]> SELECT * FROM t1;
+-----+------+
| k   | v    |
+-----+------+
|   1 |    0 |
|  10 |    0 |
| 100 |    0 |
+-----+------+
3 rows in set (0.00 sec)

この状態で2つ目のトランザクションで全行を v = 2 に変更します。

[2]> UPDATE t1 SET v = 2;
Query OK, 3 rows affected (0.02 sec)
Rows matched: 3  Changed: 3  Warnings: 0

1つ目のトランザクションで k = 1 の行を排他ロックありで SELECT すると

[1]> SELECT * FROM t1 WHERE k = 1 FOR UPDATE;
+---+------+
| k | v    |
+---+------+
| 1 |    2 | <== REPEATABLE READ でも新しい値!
+---+------+
1 row in set (0.01 sec)

REPEATABLE READ の分離レベルは他のトラザクションがコミットした最新の値をすぐ得てしまうという ノンリピータブルリード/ファジーリード は発生しないはずですが、見えてしまいました。

このように LOCK IN SHARE MODEFOR UPDATE が明示されているロック読取りを使うと、REPEATABLE READ の分離レベルであっても READ COMMITTED 同様のファジーリードが発生します。

ギャップロックや挿入は?

 第7回ギャップロックと消えるロック でも解説していますが、前述のようにロック読取りで READ COMMITTED 相当の挙動を見せたあとでも、その後の他のクエリは REPEATABLE READ の分離レベルとして想定どおりギャップロックが付与されます。

REPEATABLE READ / READ COMMITTED と違うところ

前述したようにロック読み取りは READ COMMITTED と同様のファジーリードになりますが、ではロック読み取り後、READ COMMITTED 相当の挙動になるのはどのあたりまでになるでしょうか? いくつか試してみます。

最初の例と同じ条件で t1 テーブルでトランザクションを開始し全行 SELECT します。

[1]> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

[1]> SELECT * FROM t1;
+-----+------+
| k   | v    |
+-----+------+
|   1 |    0 |
|  10 |    0 |
| 100 |    0 |
+-----+------+
3 rows in set (0.00 sec)

その後2枚目の端末で k = 1 の行を v = 1 に更新します。

[2]> UPDATE t1 SET v = 1;
Query OK, 3 rows affected (0.01 sec)
Rows matched: 3  Changed: 3  Warnings: 0

この状態で最初の端末で k = 1 行でロック読取りを実行すると

[1]> SELECT * FROM t1 WHERE k = 1 LOCK IN SHARE MODE;
+---+------+
| k | v    |
+---+------+
| 1 |    1 |
+---+------+
1 row in set (0.00 sec)

[1]> SELECT * FROM t1;
+-----+------+
| k   | v    |
+-----+------+
|   1 |    0 |
|  10 |    0 |
| 100 |    0 |
+-----+------+
3 rows in set (0.00 sec)

このように ロック読み取りのクエリはコミットされている最新の値、通常読み取りのクエリは REPEATABLE READ 通りトランザクションで最初に読んだ値 となります。

この現象は値をリストするだけではなく、当然 SUM(col) などの集約であってもロック読み取りが指定されているかどうかで影響を受けます。このままの状態で実行すると

[1]> SELECT SUM(v) FROM t1 LOCK IN SHARE MODE;
+--------+
| SUM(v) |
+--------+
|      3 |
+--------+
1 row in set (0.00 sec)

[1]> SELECT SUM(v) FROM t1;
+--------+
| SUM(v) |
+--------+
|      0 |
+--------+
1 row in set (0.00 sec)

全行ロック読取りの SELECTSUM したことで SUM(v) が 3 になるときと、0 のままの2種になっていることが確認できます。

こうして結果的に一つのトランザクション内でロック読取りと通常読取りで2種の値を恒常的に返すという REPEATABE READ / READ COMMITTED の純粋な定義とも異なる挙動となり、トランザクション内部での値の一貫性の維持の責務をアプリケーション側に委ねた状態となります。

第10回まとめ

LOCK IN SHARE MODE / FOR UPDATE が付いたロック読取りの値は REPEATABLE READ ではなく READ COMMITED 相当のファジーリードになる。
ー かつ通常読取りは REPEATABLE READ で動作し続けるため、ロック指示あるなしで「新旧2種の値を得る」という挙動になる。

いかがでしたでしょうか? 今回お届けしたロック読取りの解説で一連の MySQL ロック入門シリーズの MySQL 編は終了となります。いよいよ次回第11回から AWS AuroraPingCAP TiDB との挙動の違いを解説していきますのでご期待ください!

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!