ノンロッキングリード / ロッキングリード
MVCC は行にロックをかけないノンロッキングリードでしか用いられない。ノンロッキングリードでは、よも取ろうとしている行が現在実行中のほかのトランザクションにより更新され、かつ最新の行データがロックされていても、MVCC によって古いバージョンのデータを行ロックせずに読み取ることができる。すでにロックされている行にアクセスする場合でも、新たに行のロックをかけないため、ロック競合が発生しない。
しかし、MVCC を利用する分離レベルである REPEATABLE-READ と READ-COMMITTED ではクエリによってはノンロッキングリードとロッキングリードを使い分けることができる。データの更新はデータページにある最新のデータに対して行わなければならないし、更新した行はトランザクションが終了するまでロックしておかなければならない。つまり、ノンロッキングリードによって過去のバージョンのデータへアクセスするだけでは更新は成立しない。更新系のクエリや SELECT … FOR UPDATE 、SELECT … LOCK IN SHARE MODE は最新のデータへのアクセスをしつつ同時にロックを獲得する処理となる。
過去のバージョンを読み取るノンロッキングリードと最新のデータを読み取るロッキングリードでは当然値が違って見えたり、ノンロッキングリードのときには見えなかった行が見えたりする。
以下に簡単な例を示す。なおこの例は REPEATABLE READ の時の挙動となる。
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
まず 1 つめのトランザクション (T1) で以下のテーブルを作成し、トランザクションを開始する。
T1> CREATE TABLE t (a INT UNSIGNED NOT NULL PRIMARY KEY);
Query OK, 0 rows affected (0.03 sec)
T1> INSERT INTO t VALUES (10), (20);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
T1> COMMIT;
Query OK, 0 rows affected (0.00 sec)
T1> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t;
+----+
| a |
+----+
| 10 |
| 20 |
+----+
2 rows in set (0.00 sec)
次に 2 つめのトランザクション (T2) で行を追加する。
T2> INSERT INTO t VALUES (15);
Query OK, 1 row affected (0.01 sec)
T2> COMMIT;
Query OK, 0 rows affected (0.00 sec)
以下からわかるように、普通の SELECT (ノンロッキングリード) ではその行は見えないが、UDPATE コマンドでは T2 によって追加された行も更新されていることがわかる (matched: 3 と Changed: 3)。その後、再び SELECT を実行すると、既存の行の値が更新されているだけではなく、行数が増えている。これは自身がロックしている行はノンロッキングリードでも最新のデータが見えるためである。つまり、ロッキングリードが混入することで参照が REPEATABLE ではなくなってしまっている。
T1> SELECT * FROM t;
+----+
| a |
+----+
| 10 |
| 20 |
+----+
2 rows in set (0.00 sec)
T1> UPDATE t SET a = a + 1;
Query OK, 3 rows affected (0.01 sec)
Rows matched: 3 Changed: 3 Warnings: 0
T1> SELECT * FROM t;
+----+
| a |
+----+
| 11 |
| 16 |
| 21 |
+----+
3 rows in set (0.00 sec)
なお、この時に T2 で SELECT を実行すると、古い値を参照することになる。
COMMIT をすることで最新のデータを読みに行く。
T2> SELECT * FROM t;
+----+
| a |
+----+
| 10 |
| 15 |
| 20 |
+----+
3 rows in set (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t;
+----+
| a |
+----+
| 11 |
| 16 |
| 21 |
+----+
3 rows in set (0.00 sec)
MVCC は便利な反面、ノンロッキングリードとロッキングリードの混在という問題が発生する。もしこのトラブルを防ぐのであれば SERIALIZABLE を使用するべきである。
ネクストキーロック
ネクストキーロックはロッキングリードにおいはファントムリードを防ぐ目的で用いられる。これは行ロックとギャップロックを組み合わせたものであれい、ここでいうギャップとは行と行の論理的なスペースのことになる。つまり、ギャップロックとは「行の間」というオブジェクトがあるものと仮定して、ロックをかけるものである。この論理的な行間をロックすることで、その行間に新たに行が挿入されるのを防げる。つまり、ギャップがロックされている場合、INSERT はブロックされる。
もし先ほどの例で T1 の分離レベルが SERIALIZABLE だった場合には、トランザクション T2 における INSERT はギャップロックにってブロックされ、T1 が完了し、ギャップロックが解放されるまで INSERT はできない。
T1> SET SESSION TRANSACTION ISOLATION LEVEL serializable;
Query OK, 0 rows affected (0.00 sec)
T1> SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
T2> SET SESSION TRANSACTION ISOLATION LEVEL serializable;
Query OK, 0 rows affected (0.00 sec)
T2> SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
T1> CREATE TABLE t (a INT UNSIGNED NOT NULL PRIMARY KEY);
Query OK, 0 rows affected (0.01 sec)
T1> INSERT INTO t VALUES (10), (20);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
T1> COMMIT;
Query OK, 0 rows affected (0.00 sec)
T1> BEGIN;
Query OK, 0 rows affected (0.00 sec)
T1> SELECT * FROM t;
+----+
| a |
+----+
| 10 |
| 20 |
+----+
2 rows in set (0.00 sec)
T2> INSERT INTO t VALUES (15); ## ここでブロックされ、処理が止まる
T1> COMMIT;
Query OK, 0 rows affected (0.00 sec)
T2> INSERT INTO t VALUES (15); ## T1 が COMMIT したため、INSERT が可能になる
Query OK, 1 row affected (35.04 sec)
この時の様子を以下の図に示す。なお、infinum と suprenum は集合の下限と上限を表すものとする。