InnoDB で Consistent Read にならないケース

一貫性のある非ロック読み取り

InnoDB では MVCC (multiversion concurrency control) という仕組みによって 一貫性のある非ロック読み取り(Consistent Nonlocking Reads)を実現してる。

トランザクションの分離レベルが REPEATABLE READ (デフォルト) の場合、同じトランザクション内で読み取る値は常に最初に読み取ったときの値(スナップショット)になる。たとえ他のトランザクションによって値が変更されても元のバージョンのデータが undo log というところに保持されているので、ロックすることなく元の値を一貫して読めるようになっている。

一貫性のある読み取りではないケース

このように同じトランザクション内では常に同じ値を取得できるのだが、例外が2つある。

  1. 同じトランザクション内でなにか変更したら、その値が読み取られる
  2. ある行を更新したら、その行の最新バージョンのデータが読み取られる(実際には更新だけでなく DML の場合だと思われる)

(…と知っているようなことを書いているけど、ここまでの内容は全部 MySQL のリファレンスマニュアルに書いてある)

ここでは 2. のケースに注目したい。

ある2つのトランザクションが同じ行を同時に更新したらどうなるだろう?

「ある行を更新したら、その行の最新データが読み取られる」のだから、片方のトランザクションでコミット済みのデータがもう片方のトランザクションで読み取られるということになる。

それを以下のように実験してみた。

分離レベルとバージョン

mysql> select @@tx_isolation ;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.01 sec)

mysql> show variables like 'version' ;
+---------------+--------+
| Variable_name | Value  |
+---------------+--------+
| version       | 5.6.22 |
+---------------+--------+
1 row in set (0.00 sec)

2つのトランザクション (1) と (2) から UPDATE する。

トランザクション(1)                                                 トランザクション(2)
----------------------------------------------------------------------------------------------------------------------

mysql> start transaction ;
Query OK, 0 rows affected (0.00 sec)

                                                                    mysql> start transaction ;
                                                                    Query OK, 0 rows affected (0.00 sec)

mysql> select id,name,stocks from products where id = 1 ;
+----+-----------+--------+
| id | name      | stocks |
+----+-----------+--------+
|  1 | product A |     99 |
+----+-----------+--------+
1 row in set (0.00 sec)

                                                                    mysql> select id,name,stocks from products where id = 1 ;
                                                                    +----+-----------+--------+
                                                                    | id | name      | stocks |
                                                                    +----+-----------+--------+
                                                                    |  1 | product A |     99 |
                                                                    +----+-----------+--------+
                                                                    1 row in set (0.00 sec)

mysql> update products set stocks = stocks - 1 where id = 1 ;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

                                                                    mysql> select id,name,stocks from products where id = 1 ;
                                                                    +----+-----------+--------+
                                                                    | id | name      | stocks |
                                                                    +----+-----------+--------+
                                                                    |  1 | product A |     99 |
                                                                    +----+-----------+--------+
                                                                    1 row in set (0.00 sec)

                                                                    mysql> update products set stocks = stocks - 1 where id = 1 ;

                                                                    # トランザクション(1)がコミットするまでロック待ち

mysql> select id,name,stocks from products where id = 1 ;                                                                                                                                                  
+----+-----------+--------+
| id | name      | stocks |
+----+-----------+--------+
|  1 | product A |     98 |
+----+-----------+--------+
1 row in set (0.00 sec)

mysql> commit ;
Query OK, 0 rows affected (0.00 sec)

                                                                    Query OK, 1 row affected (10.03 sec)
                                                                    Rows matched: 1  Changed: 1  Warnings: 0

                                                                    mysql> select id,name,stocks from products where id = 1 ;
                                                                    +----+-----------+--------+
                                                                    | id | name      | stocks |
                                                                    +----+-----------+--------+
                                                                    |  1 | product A |     97 |
                                                                    +----+-----------+--------+
                                                                    1 row in set (0.00 sec)

                                                                    mysql> commit ;
                                                                    Query OK, 0 rows affected (0.00 sec)

ここで学習したこと

  • InnoDB はロックせずに一貫性のある読み取りができる
  • これは MVCC という仕組みのおかげ
  • ただし一貫性のある読み取りにならないケースもある

はー、今日は MySQL のリファレンスたくさん読んだ。 MySQLソースコードも読みたい。