MVCCとは
MVCC(Multi-Version Concurrency Control)は、読み取りと書き込みが互いを待たせることなく並行して処理を行えるように、トランザクションごとに一貫したスナップショットを提供する仕組みです。
InnoDBでは、行の更新時にレコードをその場で書き換える一方、更新前の状態を再構築するための情報をundoログに記録します。削除時も直ちには物理削除せず、レコードに削除マークを付与してundoログに変更前の状態を残します。
読み取り処理は、トランザクション開始時などに作成したRead Viewを手がかりに、各レコードのトランザクションIDと照合して参照すべきバージョンを選択します。最新レコードが参照すべき時点より新しい、または未コミットである場合は、undoログの情報を適用して過去のバージョンを復元して読み取ります。
Read Viewの構造
Read Viewは、読み取り処理で各レコードの可視性を判定するためにメモリ上に構築される内部データ構造です。トランザクションを管理するメモリ上のオブジェクトに紐づけて保持されます。作成時点でアクティブだったトランザクションIDや境界値を記録し、どの変更まで参照できるかを判定します。主要なフィールドは次のとおりです。
| フィールド | 役割 |
|---|---|
m_up_limit_id | これより小さいトランザクションIDの変更はすべて可視。アクティブなトランザクションが存在する場合はm_idsの最小値、存在しない場合はm_low_limit_idと等しい。 |
m_low_limit_id | これ以上のトランザクションIDの変更はすべて不可視。Read View作成時点で次に発行されるトランザクションID。 |
m_ids | Read View作成時点で実行中だったトランザクションIDのリスト。これらの変更は未コミットとして扱われる。 |
m_creator_trx_id | Read Viewを作成したトランザクション自身のID。自身の変更は常に可視。 |
m_low_limit_no | purge処理の判定用。これより小さいトランザクション番号のundoログは不要と判断される。 |
可視性の判定
対象レコードのトランザクションID(DB_TRX_ID)をidとし、Read Viewの情報と照合して次の順序で可視性を判定します。
id < m_up_limit_idまたはid == m_creator_trx_idであれば可視。Read View作成前にコミット済み、または自トランザクションによる変更であるため。id >= m_low_limit_idであれば不可視。Read View作成後に開始されたトランザクションによる変更であるため。m_idsにidが含まれていれば不可視。Read View作成時点で未コミットだったため。- 上記のいずれにも該当しなければ可視。Read View作成前にコミット済みの変更であるため。
レコードが不可視と判定された場合、DB_ROLL_PTRを使ってundoログを過去へたどり、上記の条件を満たす可視なバージョンを復元します。
システムカラムとundoレコード
クラスタ化インデックスのリーフレコードには、MVCCを制御するためのシステムカラムが付与されています。
| カラム | サイズ | 役割 |
|---|---|---|
DB_TRX_ID | 6バイト | レコードを最後に挿入または更新したトランザクションID |
DB_ROLL_PTR | 7バイト | undoログ内の直前バージョンを指すポインタ |
DB_ROW_ID | 6バイト | 主キーが存在しない場合に自動生成される行ID |
DB_TRX_IDは、読み書きトランザクションの開始時にグローバルカウンタから昇順で発行されます。DB_ROLL_PTRはundoログ内のアドレスを指しており、レコードが更新されるたびに過去のバージョンへ連鎖するundoチェーンが形成されます。
レコードの操作種別ごとのシステムカラムとundoレコードの紐づきは次のとおりです。
| 操作 | レコード | DB_ROLL_PTR |
|---|---|---|
| INSERT | 新規作成 | ロールバック時に行を削除するためのundoレコード |
| UPDATE(主キー以外) | 同一位置で書き換え。更新前の値はundoログへ退避 | 退避先のundoレコードへ付け替え |
| UPDATE(主キー変更) | 旧レコードに削除マークを付け、新レコードを挿入 | 新レコードは挿入用、旧レコードは削除用のundoレコード |
| DELETE | 物理削除せず削除マークを付与 | 削除前の状態を保持するundoレコード |
セカンダリインデックスの可視性判定
セカンダリインデックスのレコードにはDB_TRX_IDやDB_ROLL_PTRが存在せず、undoチェーンもありません。そのため、ページヘッダに記録された最新の変更トランザクションIDであるPAGE_MAX_TRX_IDを利用して可視性を判定します。
判定手順は次のとおりです。
- レコードに削除マークが付いている場合は、セカンダリインデックスのみでは可視バージョンを判断できないため、クラスタ化インデックスを参照します。
- 削除マークがなく、
PAGE_MAX_TRX_ID < m_up_limit_idであれば、ページ内の全レコードがRead View作成前にコミット済みとみなせるため、クラスタ化インデックスを参照せずにレコードをそのまま返します。 PAGE_MAX_TRX_ID >= m_up_limit_idの場合は、未確定の変更が含まれている可能性があるため、クラスタ化インデックスを参照し、DB_TRX_IDとDB_ROLL_PTRを用いて可視バージョンを復元します。
不要バージョンのpurge
削除マークが付いたレコードや更新前の古いバージョンは、過去のスナップショットを参照しているRead Viewが存在する間は保持し続ける必要があります。
参照するRead Viewがすべて終了して不要になったレコードやundoログは、バックグラウンドで動作するpurgeスレッドによって回収されます。削除マーク付きレコードは物理削除され、対応するundoログ領域は再利用のために解放されます。
purgeスレッドは、自分用のRead Viewであるpurge viewを持ちます。開いているRead Viewのうち最も古いものをコピーして更新し、そのm_low_limit_noより小さいトランザクション番号のundoログや削除マーク付きレコードを回収します。
分離レベルとRead Viewのライフサイクル
Read Viewをいつ作り、いつまで使うかは、トランザクション分離レベルによって異なります。
READ UNCOMMITTED
Read Viewによる可視性判定を行いません。ページ上の最新レコードをそのまま読み取るため、コミット前の変更が見えてしまうダーティリードが発生します。
READ COMMITTED
SELECTを実行するたびに新しいRead Viewを作成します。クエリごとにスナップショットが更新されるため、同一トランザクション内であっても、他トランザクションがコミットした最新の値が次回のSELECTで反映されます。
REPEATABLE READ
トランザクション内で最初にSELECTを実行した時点、またはSTART TRANSACTION WITH CONSISTENT SNAPSHOTを実行した時点でRead Viewを作成します。
このRead Viewはトランザクションがコミットまたはロールバックされるまで保持され、後続のクエリでも同じスナップショットを使い続けます。そのため、他トランザクションが更新をコミットしても結果は変わりません。自トランザクションによる変更はm_creator_trx_idによって常に見えます。
SERIALIZABLE
autocommitが無効な明示的トランザクションでは、通常のSELECTが共有ロック付きの読み取りに置き換わり、Read Viewによる読み取りは行いません。対象行が他トランザクションによって更新中の場合は待機が発生します。一方、autocommit=1の単独SELECTに限り、ロックを取得せずRead Viewで読み取ります。