はじめに
この記事では、MySQL の MVCC がどういう仕組みで実現されているかということを説明します。
トランザクション分離レベル
分離レベル | ダーティ・リード | ファジー・リード | ファントム・リード |
---|---|---|---|
READ UNCOMMITTED | 〇 | 〇 | 〇 |
READ COMMITTED | × | 〇 | 〇 |
REPEATABLE READ | × | × | 〇 |
SERIALIZABLE | × | × | × |
分離レベルという言葉はDBMS一般の概念で、「トランザクション同士がどの程度分離しているか」を示します。
トランザクションというのは処理の単位であり、トランザクション同士が干渉しあうことは一般的に望ましいことではありませんが、トランザクションがほかのトランザクションと「どの程度独立しているか」ということには4つのレベルがあり、それを分離レベルと呼びます。
MySQL の分離レベルはデフォルトでは REPEATABLE READ です。
一般的にこの分離レベルでは、「同一トランザクション内で、他トランザクションによる追加・削除の影響を受ける」 という意味の ファントム・リード が発生しますが、MySQL ではロックをとらずともこの影響が発生しないよう、 MVCC という仕組みが備わっています。
ファントム・リードを起こさないための仕組みというより、その影響を受けないようにするための仕組みという感じが近いかもしれません。
MySQL における MVCCのしくみ
MVCC(Multi Version Concurrency Control) は分離レベルが READ COMMITTED 、REPEATABLE READ のときに使われる機能で、複数のバージョンのデータを持ち、トランザクションに対して適切なバージョンのデータを返すことで、他トランザクションの処理の影響を受けないようにしています。
ある行データについて、仕掛り中のトランザクションごとに複数管理しておけば、あるトランザクションから行データを要求されたときに、そのトランザクションが見える"べき"値を返すことができる、といった感じです。
このMVCCを実現させているのは Undo ログ という仕組みですが、これを下に図示します。
主キーインデックスのリーフノードには様々な情報が格納されています。
その値を変更したトランザクションのIDを示すTRX_ID (transaction_id) であったり、MySQL のインデックス構造は B+Tree であるため、隣同士のインデックスを参照しあうためのポインタであったりします。
その中のひとつのポインタが、Undo ログへの参照を持っています。
Undo ログの中には複数世代の行データがリンクでつながって管理されており、リンクをたどるほど古い世代の値になります。
それぞれの値は、どのトランザクションIDによって更新された値なのかがわかるよう、TRX_ID と古いデータが一緒に管理されています。
TRX_ID はトランザクションが発行されるたびにインクリメントされて採番されるようで、これによってトランザクション同士の新旧比較が可能になっています。
MVCC の例
ここではMVCCの挙動をがんばって表現してみます。
まず、TRX_ID : 96 でなんらか SELECT 文(図右上)が発行されたとします。
主キーインデックスのリーフノードにたどりつき、その内容を見てみると DELETED フラグが立っているためもう削除済みであることがわかります。
削除済みなんですが、それを実行したトランザクションは TRX_ID : 100 と、自身(96)より新しいトランザクションによる処理であるため、自分(96)はその結果に影響をうけないようにしたいです。
そこで利用されるのが Undo ログで、インデックスのリーフノードの ROLL_POINTER の宛先の Undo ログを確認します。
すると、リンク先には1世代前の値が登録されています。
しかし、これも TRX_ID : 98 であり、自身より新しいため、さらにリンクをたどります。
次の値は TRX_ID : 95 であり、これは自身より古いトランザクションによる更新であるため、ここに登録されている値を取得します。
このように、複数世代の値を管理することで分離レベルが REPEATABLE READ であってもファントム・リードを影響を受けないようにしています。
MVCCの注意点
詳細は以下記事を参照いただければとおもいますが、ロストアップデートという問題を起こさないようにするため、通常の SELECT
か、更新ロックつきで SELECT ... FOR UPDATE
とするかで挙動がかわるようです。
実装時に気をつけておくこととしては、MySQL はデフォルトの分離レベルでは、
- 単に
SELECT
するだけならFOR UPDATE
はつけなくてもファントムは MVCC によって発生しない。 - 同一トランザクション内で更新もするときにはロストアップデートを防ぐために
SELECT ... FOR UPDATE
とする。
といったところでしょうか。
そのほか懸念事項
また、別の注意点として、Undo ログの存在がパフォーマンスに悪影響を及ぼす可能性を念頭にいれておく必要があるとおもいます。
Undo ログのチェーンはトランザクションごとに作られるらしいのですが、これは基本的には定期的にパージされているようです。
トランザクションが短い時間で完了するものばかりであれば、Undo ログの短い時間間隔でパージして問題ないですが、時間の長いトランザクションがあると、Undo ログがパージできず、Undo ログのサイズが肥大化していくことが懸念されます。
MySQL のパフォーマンスの問題が発生したときにはこの点を疑ってみてもいいかもしれません。