redoログとは
redoログは、InnoDBがクラッシュリカバリを実現するための仕組みです。INSERT・UPDATE・DELETEでバッファプール内のページを更新すると、そのページをフラッシュする前に、どのページのどの位置に何を書いたかという変更履歴をredoログに記録します。もしサーバーがクラッシュしても、再起動時にチェックポイント以降のredoログを古い順に適用すれば、ディスクへ反映しきれていなかった変更を復元できます。
B-tree上のページ10のuser_id='user01'のnameをAliceからBobへ更新すると、コミットした直後は次のようになります。バッファプール上のページ10はすでにuser01,Bobですが、データファイル上の同じページはまだuser01,Aliceです。ディスクへ永続化されているのは、ページ10のnameをBobにしたという変更履歴だけです。
LSNとチェックポイント
LSN(Log Sequence Number)は、redoログの論理的な位置を表す番号です。ログはLSNの昇順で生成され、リカバリでもLSNの昇順に適用します。途中を飛ばすことはできません。
チェックポイントLSNは、このLSN以前の変更はすでにディスク上のデータページへ反映されているという境界を表します。チェックポイントより前のログ領域は上書きして再利用することができます。チェックポイントを進めるには、そのLSNまでの変更をすべてデータページへフラッシュしておく必要があります。
ログバッファ
ログバッファは、redoログファイルへ書き出す前にredoログレコードを一時的に置くメモリ領域です。コミットやページ更新で生成されるredoログは、まずここに書かれ、その後バックグラウンドのログライタースレッドがディスクへフラッシュします。多くの行を更新・挿入・削除するトランザクションほどredoログも増えますが、ログバッファを大きくしておくと、コミットまでディスクへの書き込みを待たずにバッファへ溜められ、ディスクI/Oの回数を減らすことができます。
トランザクションの変更がコミットされると、その変更を記述したredoログが生成されます。書き込みは大まかに次の手順です。
- ログバッファ内に書き込む領域(バイト数)を予約する。予約によりstart_lsnからend_lsnの範囲が割り当てられる。
- 予約した範囲へredoログデータを書き込む。
- 書き込み後、「start_lsnからend_lsnまで書き込み済み」という情報をrecent writtenバッファに登録する。
複数スレッドが同時にログバッファへ書くと、書き終わる順序がLSNの並びと一致しないことがあります。たとえばスレッドAがLSN100〜150、スレッドBが150〜200を予約したとき、Bが先に書き終わってAがまだ途中、という状態が起こり得ます。ところがログライターは、リカバリで先頭から順に適用できるように、ディスクへLSNの若い順に書かなければなりません。100〜150がまだ埋まっていないのに150〜200だけをディスクへ書くことはできず、Aの完了を待つ必要があります。
recent writtenバッファは、各スレッドが「自分の範囲を書き終えた」ことを登録する補助構造です。ログライターは、登録されたリンク(start_lsn → end_lsn)の連鎖をたどることで「このLSNより前はすべてバッファに書き込み済み」と判断でき、その境界までをディスクへフラッシュします。リンクが途切れている箇所では該当スレッドの完了を待ちます。これで、複数スレッドの並行書き込みと、ログライターの順序保証が両立します。
ログバッファをいつディスクへフラッシュするかは、innodb_flush_log_at_trx_commitで設定できます。
| 値 | 動作 |
|---|---|
| 0 | 1秒に1回程度フラッシュする。コミット時にはフラッシュしない。クラッシュ時に直近1秒分のコミットが失われ得る |
| 1 | コミットのたびにフラッシュし、OSのfsyncまで完了させる。耐久性が最も高い |
| 2 | コミットのたびにOSのキャッシュへは書くが、fsyncは毎回は行わない。OSがクラッシュするとログが失われ得る |
クラッシュリカバリ
クラッシュ後の再起動時、InnoDBはリカバリ処理を行います。チェックポイントLSNから、ディスク上のredoログに記録された末尾まで、LSNの順にredoログを適用します。まずは各ログレコードに記録された変更履歴に従い、該当ページを読み込んでバッファプール上で変更を反映します。反映されたページはダーティになり、その後のフラッシュでディスクへ書き出されます。こうして、未フラッシュだったダーティページの内容がredoログを通じて復元されます。
undoログとは
undoログは、1つの読み書きトランザクションに紐づくundoレコードの集まりです。undoレコードは、クラスタ化インデックスの行に対する最新の変更を打ち消す情報が1件分入ったものです。行を1回変更するたびにundoレコードが1件増え、それらはundoログにまとめて置かれます。
InnoDBでは主に2つの用途があります。
1つはROLLBACK時の取り消しです。トランザクションがコミットされずに終わった場合、そのトランザクションが行った変更をundoログに基づいて打ち消し、データを変更前の状態へ戻します。
もう1つはMVCC(Multi-Version Concurrency Control)です。ある時点のスナップショットを見ている読み取りからは、他のトランザクションがすでにコミットした最新の行がまだ見えてはいけないことがあります。そのとき、undoログに残された古いバージョンをたどって、その読み取りから見えるべきバージョンを復元します。
undoログの場所
通常テーブル向けのundoログはundoテーブルスペースのrollback segmentに、一時テーブル向けはグローバル一時テーブルスペースのrollback segmentに割り当てられます。
rollback segmentはundoスロットの配列を持ち、スロットには先頭undoページの番号が入ります。undoページとundoレコードは同じテーブルスペース上の別領域にあります。undoレコードはundoページに複数件載ります。1ページに収まらなくなると、同じundoログに次のページをつなぎます。
undoログの種類と寿命
1つのトランザクションには、必要になった種類だけundoログが割り当てられます。通常テーブルのINSERTだけなら1本です。通常テーブルと一時テーブルの両方で、INSERTとUPDATE(またはDELETE)を行うと4本になります。
通常テーブル向けのundoログはundoテーブルスペースに置かれ、ROLLBACKとMVCCの両方に使われます。一時テーブル向けのundoログはグローバル一時テーブルスペースに置かれます。一時テーブルは作ったセッションからしか見えないので、他のトランザクションが古い値を読む必要はなく、ROLLBACKにだけ使われます。
通常テーブルでは、INSERTとUPDATEでコミット後にundoログを残すかが異なります。
INSERT用のundoログは、コミット完了時に破棄または再利用されます。コミット後に他のトランザクションが読む可能性がある古い値が存在しないからです。UPDATE用(DELETEを含む)のundoログは、コミット後も残します。他のトランザクションがまだ更新前の値を読んでいることがあるからです。他のトランザクションから読まれなくなったら回収されます。
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で読み取ります。