MVCC
※ ここには InnoDB の MVCC 実装についてメモしておく
MVCC (Multi Version Concurrency Control) とは、トランザクション分離レベルを実現するための機能であり、行データの過去のバージョンを保存しておく仕組み。トランザクションが参照できるデータに一貫性を持たせようと思うと、最新のデータを参照するだけでは不足なので、何らかの方法で古いデータを参照し続ける必要がある。そうでなければ、あるトランザクションの最中に他のトランザクションにデータを書き換えられたら、書き換えられたデータを参照することになってしまい、一貫性がなくなる。 SERIALIZABLE MVCC は利用されない。退避されたデータを参照するまでもなく、そもそも操作中の全ての行アクセスでロックをかける
REPEATABLE READ MVCC が利用される。トランザクションの開始から終了まで、行レコードは同じ値を参照できるようにしておく必要がある
READ COMMITED MVCC が利用される。他のトランザクションにレコードを更新されても、コミットされるまでは古いレコードを読んでおく必要がある
ステートメント単位で一貫性が保証される、らしい
どういうことだろうか
READ UNCOMMITED MVCC は利用されない。全てのデータを最新の状態で取得するので、データを退避させておく必要がない
UNDO ログ
MVCC では古いバージョンのデータを参照することができる。そのデータの実体は、最新のデータからポインタされた UNDO ログ として保持される。 下図では、トランザクション T1 が始まった後、T2 が開始し、レコードのデータを書き換える。このとき、T1 においてレコードのデータの一貫性を保つために UNDO ログ が挿入される。UNDO ログ は、T2 から複数回書き込みが行われるたびに生成され、各々が ロールバックポインタ で参照できるようになっている ( T2 がロールバックした場合は、UNDO ログ を順番に適用してデータを戻す)。 UNDO ログ の削除 (パージ) のタイミングは、T2 がレコードを書き換えた (COMMIT した) よりも前に開始していたトランザクションが全て終了したら、である。そのタイミングで、レコードの古いバージョンを参照するトランザクションはいなくなるので、UNDO ログ は不要となる。下図の場合、T1 が T2 が INSERT を行うより前に開始しているので、T2 が終了したタイミングではなく、T1 が終了したタミングでパージが行われる。 https://gyazo.com/9d3722f3f6a92ff55c6905d552a1870e
行レコードに対する操作には、ノンロッキングリード と ロッキングリード がある。前者は SELECT 等の行ロックをかける必要がない操作、後者は UPDATE や SELECT ... FOR UPDATE 等の、最新データにアクセスする必要がある、行ロックをかける操作である。 UNDO ログ の読み取り、すなわち MVCC はノンロッキングリードでのみ利用される。上図の場合、T2 によって ID=2 のレコードがロックされていても、T2 がロックするのは最新の行データなので、影響は受けない。逆に、T1 が ID=2 の UNDO ログ を参照する際にも、行ロックをかける必要はない。この ノンロッキングリード をしている限りでは、REPEATABLE REAd, READ COMMITTED で ファントムリード が発生することはない。 一方、T1 にて ロッキングリード、すなわちデータの更新や SELECT ... FOR UPDATE 等を利用すると、UNDO ログ は読み取られず最新データにアクセスされ、さらに行ロックがかかる。これによって、ファントムリード が発生してしまう。 つまり、操作によって最新のデータにアクセスする場合と UNDO ログにアクセスする場合がある。REPEATABLE READ, READ COMMITED では、このせいでファントムリードが発生する場合がある。
https://gyazo.com/7ffaaabfe7306fe1f0095dd5a1192711
上図のように、REPEATABLE READ では、T1 から T2 で INSERT したデータが、ロッキングリード では見えてしまう。 この時、T1 は ノンロッキングリード で、T2 がデータをインサートした後も、古いデータしか参照しないようになっている。例えば T2 が 1 や 10 のレコードを更新しても古いデータに参照できるのは UNDO ログ のおかげだが、新しく挿入されたデータにアクセスされないのはなぜか。 これは、トランザクションID という仕組みが利用されているためである。InnoDB の行レコードには、通常では見えない以下の値が同時に格納されている。
DB_TRX_ID 6byte。その行レコードを挿入、または更新した最後のトランザクションのトランザクション識別子
DB_ROW_ID 6byte。行の挿入ごとに単調増加する ID。クラスターインデックスに保持される
MVCC では、今現在のトランザクション ID を見て、そのトランザクション ID より新しいトランザクション ID をもつデータにはアクセスしないように制御される。これは、トランザクション制御にて スナップショット を取り参照していると表現されるが、実際にはこのように スナップショット をとっているわけではなく、ID を参照してアクセスするか否かを決めているだけである。このトランザクション ID を取得するには、以下のようなレコードを発行する。 code:sql
mysql> SELECT trx_id FROM information_schema.innodb_trx WHERE trx_mysql_thread_id = CONNECTION_ID();
+-----------------+
| trx_id |
+-----------------+
| 281479620302640 |
+-----------------+
1 row in set (0.00 sec)
また、innodb_lock_monitor でロッキングの状態を確認できるようだ。
分離レベル SERIALIZABLE は、標準 SQL 仕様にてファントムリードは許可されていていないため、ロッキングリード でもファントムリードが発生しないようにする必要がある。これは、ネクストキーロック にて実現されている。ネクストキーロック は、行ロック と ギャップロック の組み合わせであり、このロックを取得すると、行レコードそのものを更新できないでなく、その間に新たにレコードを挿入することもできなくなる。このため、ネクストキーロック が行われている範囲には新たにデータを挿入することができなくなる。 https://gyazo.com/4d5aeac85be34af49bc9cd0ad85b742d