まるまるこふこふ

数々の次元が崩壊し、全ての生命が塵と化すのを見てきた。私ほどの闇の心の持ち主でも、そこには何の喜びも無かった。

REPEATABLE READをスナップショットという言葉使って語るのは今すぐやめるべき

MYSQLのデフォルトのトランザクション分離レベルは
REPEATABLE READです。

REPEATABLE READにおいてもファントムリードが起こることは
避けられませんが、MySQLのREPEATABLE READでは、
MVCCを採用しているため、ファントムリードは起こりません。

MVCCにおいてはよく「トランザクション毎にスナップショットを作成し、それを 参照することで他のトランザクションからの干渉を受けない」といった説明がされます。

スナップショットという表現のため、我々はトランザクションが始まるたびに、
そのトランザクションが扱うテーブルのメモリコピーをいちいち作成するような
イメージを受けますが、実はそのようなスナップショットという表現のイメージと
実装は異なっています。

MVCC とは

MVCC(MultiVersion Concurrency Control)という名の通り、
バージョニングによりトランザクションの独立性を保ちます。

以下は MySQLInnoDB のMVCC 実装の話です。

MySQLはシステムバージョン番号を持っています。これはトランザクション
1つ開始されるたびに +1 インクリメントされます。これによって
トランザクションは一意なシステムバージョン番号を持っています。
古いトランザクションより新しいトランザクションの方が
大きいシステムバージョン番号を持ちます。

InnoDB上の各レコードは、内部的に2つの値を持っています。1つが行が
作成された時のシステムバージョン番号、もう1つが行が削除された時の
システムバージョン番号です。

これを踏まえて、CRUDの各処理において MySQLがどのような挙動をしているかを示します。

SELECT

SELECT時に、トランザクションは自分の持っているシステムバージョン番号と
同じかそれ以前のバージョンの行しか取得しません。

これにより自分より新しいトランザクションのレコード作成や更新の影響を
受けません。 (なぜ更新の影響まで受けないかはUPDATEの項で説明します)

またトランザクションはSELECT時に、行の削除バージョンが未定義か、
自分の持っているシステムバージョン番号より新しいバージョンの行を
取得します。 これにより自分より新しいトランザクション
レコード削除の影響を受けません。

INSERT

トランザクションはINSERT時に自分のシステムバージョン番号を
新規レコードに記録します。

DELETE

トランザクションはDELETE対象のレコードに、自分のシステムバージョン番号を記録します。
自分より古いトランザクションがこのレコードを参照する可能性があるため、
この段階では物理的にレコードはまだ削除されません。
SELECT時の挙動によりDELETEしたレコードはトランザクションから
あたかも削除されたかのように見えなくなります。

UPDATE

UPDATE時、トランザクションUPDATE対象の行をコピーして新しい行を作成します。

新しい行には自分のシステムバージョン番号を記録し、古い行には
削除バージョンとして自分のシステムバージョン番号を記録します。

これにより古いトランザクションはUPDATEされる前の古いレコードを参照し、
新しいトランザクションはUPDATEされた新しいレコードを参照することで、
トランザクションの独立性を保ちます。

終わり

こうしたMVCCの仕組みによりREPEATABLE READな MySQLInnoDB
行をロックすることなく、またテーブル全体のスナップショットを取るような
マネもせず、トランザクションの独立性を担保しています。

こうした仕組みはスナップショットと呼ばれ、各種RDBMSの公式docでも
使用されていますが、スナップショットという言葉のイメージに惑わされ、
InnoDBのREPEATABLE READ の挙動を勘違いする人たちが後を絶たないので、
皆様も今すぐスナップショットという言葉でREPEATABLE READを理解するのでなく、
バージョニングの概念を使って REPEATABLE READ の仕組みを理解するようになってほしい。