MySQLの一貫性読み取りについて調べてみました

ロックあたりを調べていたら気になったので、ついでに調べました。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.2.4 一貫性非ロック読み取り

ポイント

  • ある時点でのデータベースのスナップショットをクエリーに提供
  • トランザクション分離レベルが REPEATABLE READ の場合は、同じトランザクション内すべてのが一貫性読み取り
  • 一部のステートメントではデフォルトの設定において一貫性読み取りが提供されない

一貫性読み取り

一貫性読み取りとは、データベースのある時点での状態をスナップショットとして保持し、トランザクション内で一貫性のあるクエリを実現する事です。公式のドキュメントには以下のような記述があります。

一貫性読み取りとは、InnoDB がマルチバージョンを使用して、ある時点でのデータベースのスナップショットをクエリーに提供することを意味します。クエリーには、その時点よりも前にコミットされたトランザクションによる変更のみが表示され、その時点よりもあとのトランザクションまたはコミットされていないトランザクションによる変更は表示されません。

実際に試してみましょう。

REPEATABLE READで一貫性読み取りを試す

トランザクションの分離レベルを REPEATABLE READ (デフォルト)にした状態で一貫性読み取りの動きを確認してみたいと思います。ちなみにトランザクション分離レベルは以下のQiitaの記事で調べたことをまとめています。

MySQLのトランザクションと分離レベル - Qiita

以下で使用する books テーブルはこのようなテーブルです。

mysql> SHOW CREATE TABLE books;
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                                                                                                                                             |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| books | CREATE TABLE `books` (
  `id` int(11) NOT NULL,
  `name` varchar(50) NOT NULL,
  `publisher` varchar(50) NOT NULL,
  `author` varchar(50) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `books_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci |
+-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

2つのトランザクションを開始し、一方で SELECT を、もう一方で INSERTbooks テーブルに行います。ステートメントの左側にトランザクション名を示しておきます。

T1> BEGIN;

T1> SELECT id FROM books;
+----+
| id |
+----+
|  3 |
|  1 |
|  2 |
+----+

                                                                T2> INSERT INTO books VALUES 
                                                                                    (4,
                                                                                     'Effective Java (3rd Edition)', 
                                                                                     'Addison-Wesley Professional',
                                                                                     'Joshua Bloch'
                                                                                    );

T1> SELECT id FROM books;
+----+
| id |
+----+
|  3 |
|  1 |
|  2 |
+----+

T1 トランザクションでのクエリ( SELECT )がトランザクション開始時点から変化していないことがわかりました。これが一貫性読み取りと言われるものですね。ちなみに、一貫性読み取りはトランザクション分離レベルによって動きが変わります。

REPEATABLE READ の場合には最初のクエリが実行されたタイミングでデータベースのスナップショットを取り、その後のクエリではそのスナップショットを使用したクエリ結果を返します。なので、上記の例にある INSERT のタイミングを変えると、異なる結果になります。

T1> BEGIN;

                                                                T2> INSERT INTO books VALUES 
                                                                                    (4,
                                                                                     'Effective Java (3rd Edition)', 
                                                                                     'Addison-Wesley Professional',
                                                                                     'Joshua Bloch'
                                                                                    );

T1> SELECT id FROM books;
+----+
| id |
+----+
|  3 |
|  4 |
|  1 |
|  2 |
+----+

T1 トランザクションの最初のクエリ前に INSERT が走ったので、 T1 トランザクションが使用するスナップショットは INSERT の内容を反映したものになっています。

一貫性読み取りが効かないステートメントもある

一部、一貫性読み取りが効かない場合もあります。例えば、 INSERT INTO ... SELECT 構文で読み取られる行はデフォルトでは一貫性読み取りにはならず、他のトランザクションで変更された内容を読み取ってしまいます。そのため、以下のような状況がありえます。

T1> begin;

T1> select * from plans_stocks;

                                                                    T2> begin;

                                                                    T2> insert into plans_stocks values (\
                                                                        -> 5,
                                                                        -> 'Practical VIM',
                                                                        -> 'Pragmatic Bookshelf',
                                                                        -> 'Drew Neil');

                                                                    T2> commit;

T1> insert into books \
        -> select * from plans_stocks;

T1> select id from books;
+----+
| id |
+----+
|  3 |
|  4 |
|  5 |
|  1 |
|  2 |
+----+

上の例では T1 トランザクションplans_stocks テーブルの読み込みを行っており、その時のスナップショットをつかった一貫性読み取りが有効になっています。その後、 INSERT INTO ... SELECT ステートメントを使って plans_stocks から値を書き込んでいますが、読み込むデータはスナップショットからではなく、 T2 トランザクションからの影響を受けています。つまり、 INSERT INTO ... SELECT 構文での SELECT には一貫性読み取りが有効になっていません。

このような場合で一貫性読み取りを有効にする方法もあるみたいです。(実際に試してはいませんが、このドキュメントの下部にその方法が書いてあります。MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.2.4 一貫性非ロック読み取り