blog

InnoDBを理解する -- トランザクション

Unrepeatable reads:トランザクションAが同じデータを複数回読み取り、その間にトランザクションBがデータを更新してコミットするため、トランザクションAが同じデータを複数回読み取ると結果...

Jun 3, 2020 · 11 min. read
シェア







トランザクション定義

トランザクションの基本要素

Atomicity: 原子性、データベーストランザクション全体が不可分の作業単位であること。

一貫性(Consistency): 一貫性、トランザクションはデータベースをある状態から次の状態へと一貫性を保ちます。

分離(Isolation): 分離、各読取り/書き込みトランザクションオブジェクトを他のトランザクションオブジェクトから分離することができます。

耐久性(Durability): 耐久性、一度トランザクションがコミットされると、その結果は永続的。

トランザクションの同時実行

ダーティ・リード:トランザクションBが更新したデータをトランザクションAが読み取り、Bがその操作をロールバックした場合、Aが読み取ったデータはダーティ・データとなります。

非再現読み取り:トランザクションAが同じデータを何度も読み取り、その間にトランザクションBがデータを更新して送信するため、トランザクションAが同じデータを何度も読み取ると結果に矛盾が生じます。

ファントムリード: トランザクションAが同じ条件クエリを何度も読み取り、その間にトランザクションBが条件を満たすデータを削除または挿入します。

標準SQLでは分離レベルを次のように定義しています:

READ UNCOMMITTEDトランザクションは、コミットされていない他のトランザクションによって変更されたデータを読み取ります。はいはいはい
READ COMMITTEDトランザクションは、コミット済みの他のトランザクションによって変更されたデータしか読み取ることができません。いいえはいはい
REPEATABLE READトランザクションはコミットされた他のトランザクションによって変更されたデータしか読むことができず、トランザクションが初めてレコードを読んだ後、他のトランザクションがそのレコードの値を変更してコミットしても、トランザクションが後で再びレコードを読むと、最初に読んだ値を読みます。いいえいいえはい
シリアライズ可能トランザクションのシリアル実行いいえいいえいいえ

トランザクションの実装

redo log

REDOログはREDOログと呼ばれ、トランザクションの原子性、永続的な

つまり、トランザクションがコミットすると、まずREDOログが書き込まれ、次にディスクページのデータが変更されます。ダウンした場合、データの復旧はREDOログを通して行われます。これはトランザクションACIDのD(Durability persistence)の要件でもあります。

実際にはメモリプール内のページデータを変更する最初の操作です。REDOログを最初に書き込むことは、ディスクデータを変更することと相対的です。

Innodb テーブルへの更新は全て、Innodb が更新操作を REDO ログに変換し、ディスクに書き込み、変更の詳細は REDO ログに記録されます。

REDOログはページオフセットやバイトなどのデータを記録する物理ログです。

REDOログをやり直すことは、実際にはコミットトランザクションによって変更されたページの物理的な操作をやり直すことです。

innodb_flush_log_at_trx_commit パラメータは、REDO ログキャッシュをディスクにフラッシュするポリシーを制御します。1: デフォルトでは、トランザクションコミットは一度 fsync オペレーションを起動しなければなりません。

0:トランザクションのコミット時にREDOログ書き込み操作は実行されず、マスター・スレッドは1秒ごとにfsync操作を実行します。

2: トランザクションがコミットされると、fsync 操作を行わずに、REDO ログのみがファイルシステムのキャッシュに書き込まれます。

トランザクションのコミット時にREDOログをディスクにフラッシュする以外に、以下のシナリオがあります。

  1. REDOログバッファがすでにメモリ容量の半分を使用しています。
  2. Async/Sync Flush Checkpoint
log block

REDOログバッファは、REDOログファイルは、REDOログブロックと呼ばれるブロックに保存され、各ブロックのサイズは512バイトです。ディスクセクタの同じサイズのREDOログブロックは、REDOログの書き込みは、原子性を保証することができますので、ダブルライト技術を必要としません。

ログブロックは、ログブロックヘッダ、ログコンテンツ、ログブロックテーラの順に3つの部分で構成されます。

ログブロックのヘッダは次のようになっています。

LOG_BLOCK_HDR_NO4ログ・バッファはログ・ブロックから構成され、内部的にはログ・バッファはログ・ブロックの配列のようなもので、LOG_BLOCK_HDR_NOはこの配列の添え字をマークするのに使われ、インクリメントされてリサイクルされます。最初のビットはフラッシュビットとして使用され、最大値は2Gです。
log_block_hdr_data_len2ログブロックの占有サイズを示します。最大は0x200で、これはログブロックが512バイトであることを意味します。
log_block_first_rec_group2新しいトランザクションの最初のログオフセット。トランザクションL1を格納した後、ログブロックに空き領域がある場合、次のトランザクションL2の内容も格納され、LOG_BLOCK_FIRST_REC_GROUPはトランザクションL2のオフセットを記録します。

LOG_BLOCK_TRL_NOのみ、ログブロックの最後に4バイト、LOG_BLOCK_CHECKPOINT_NOと一致。

log group

REDOロググループは、複数のREDOログファイルREDOログファイルで構成される論理的な概念で、各ロググループのログのサイズは同じです。デフォルトのREDOロググループは、ib_logfile0、ib_logfile1で構成されています。InnoDBバージョン1.2以前では、REDOロググループの合計サイズは4GB未満であるべきでしたが、InnoDBバージョン1.2以降では512GBに増加しました。

ログ・ブロックへの書き込みはREDOログ・ファイルの最後の部分に追加され、REDOログ・ファイルがいっぱいになると、ラウンドロビンを使って次のREDOログ・ファイルが次に書き込まれます。

アーキテクチャのセクションで述べたように、チェックポイントによってページがディスクにフラッシュされると、対応するREDOログは不要になり、その領域を上書きして再利用することができます。

各REDOグループの最初のREDOログファイルの最初の2kbは、ログブロックの情報を保存するのではなく、ログファイルのヘッダ、チェックポイント情報を保存します。

REDOログはシーケンシャルに書き込まれるため、非常に高速ですが、チェックポイント後は、シーケンシャルではない最初のログファイルのヘッダのチェックポイントフラグを更新する必要があります。

REDOグループの最初のREDOログファイル以外の最初の2KBは情報を保存せず、空として予約されています。

REDOログファイルの最初の2Kコンテンツには、以下のデータが順番に格納されています。

CHECKPOINT BLOCKの内容に注目

LOG_CHECKPOINT_LSNはチェックポイントの値を示し、この値より小さいLSNを持つページはディスクに書き込まれました。2つのCHECKPOINT BLOCKSがあり、InnoDBはチェックポイントの値を交互に更新します。

そのため、チェックポイントブロックの書き込みが失敗しても、最後に記録されたチェックポイントからクラッシュリカバリが開始され、データベーストランザクションを正しくリカバリすることができます。

リカバリのために、InnoDBはこれらの2つのCHECKPOINT BLOCKSを読み、その値より大きいREDOログをリカバリするために、2つのうち大きい方のLOG_CHECKPOINT_LSNを取る必要があります。

LSN

LSNはログシーケンス番号で、各REDOログのシリアル番号です。

LSNは複数のオブジェクトに存在し、異なる意味を表します。

  1. はREDOログ書き込みの総数を表します。

    LSNは単調に増加し、log_sysに格納されます。

    例えば、新しいログの長さがlenであれば、log_sys->lsn += lenとなります。

  2. ページの最後に更新された位置を表します。各ページはヘッダにFIL_PAGE_LSNを持ち、ページが最後に更新されたときのLSNのサイズを記録し、ページが回復操作を受ける必要があるかどうかを判断するために使用できます。

パラメータ: innodb_fast_shutdown, データベースのシャットダウン操作を制御します。

0:シャットダウン、全ての完全なパージと挿入バッファのマージ、そして全てのダーティページをディスクにリフレッシュする必要があります。

1:デフォルト、ダーティページをディスクにフラッシュするだけ

2:ログがログファイルに書き込まれていることを確認するだけで、次の起動時に、リカバリ操作が実行されます。

パラメータ: innodb_force_recovery, データベースのリカバリ操作の制御

デフォルトは0で、リカバリが必要な時に全てのリカバリ操作が実行されることを示します。その他の設定値はリストされていません。

undo

アンドゥ・ログはトランザクションのアトミック性を保証し、トランザクションのロールバックとMVCC機能に役立ちます。

アンドゥは論理ログで、データ行ごとに各操作の逆を記録します。

ロールバック操作では、挿入の場合は削除を、削除の場合は挿入を、更新の場合は逆更新を行います。

undo

InnoDBはアンドゥ・ログをデータと見なし、ページ経由でアンドゥ・ログを保存します。

ロールバックセグメント

ロールバックセグメントもまた、ページに格納されるセグメントオブジェクトで、以下の内容を持ちます。

1つのUNDOセグメントで1つのトランザクションを管理でき、1つのロールバックセグメントで1024のUNDOセグメントを管理できます。

ロールバックセグメントが1つしかないInnoDB1.1では、最大1026の同時トランザクションをサポートします。

InnoDB 1.1 からは、最大 128 のロールバックセグメントがサポートされます。

FIL_PAGE_TYPE_SYS は、全てのロールバックセグメントが配置されているページを記録します。

UNDOセグメント

UNDO セグメントは、アンドゥのログが実際に保存される場所です。これは実際には UNDO ページの連鎖したテーブルです。チェーンテーブルの最初の UNDO ページは以下の部分で構成されています:

  • UNDO LOG PAGE HEADER
  • UNDO LOG SEGMENT HEADER
  • UNDO

UNDO LOG PAGE HEADERは次のように表示されます。

UNDO LOG SEGMENT HEADERは以下のようになります。

UNDO LOG SEGMENT HEADERは、UNDOページチェーンテーブルの最初のUNDOページにのみ格納され、他のUNDOページの対応する位置は空のままです。

アンドゥ記録の構造

それぞれのアンドゥ記録は次の2つの部分で構成されています。

  • UNDO LOG HEADER
  • UNDO LOG RECORD

UNDO LOG HEADERは次のように表示されます。

トランザクションがオープンされると、UNDOセグメントとともに一意の厳密な増分トランザクションIDが割り当てられ、そのTRX_UNDO_STATE変数はTRX_UNDO_ACTIVEに設定されます。

注:InnoDBはアンドゥログをデータとして扱い、UNDOページは通常のデータページと共に管理され、LRUルールに基づいてメモリからフラッシュされ、その後ディスクから読み込まれます。同様に、UNDOログに対する操作はREDOログに記録される必要があります。

例えば、挿入操作に対して、REDOログは挿入操作を記録するだけでなく、元に戻す挿入操作を記録する必要があります。

リカバリする時、InnoDBはコミットされていないトランザクションとロールバックされたトランザクションを含む全てのトランザクションをやり直します。これらのコミットされていないトランザクションは、元に戻すログを介してロールバックされます。

UNDOページの再利用

UNDOページはトランザクションのコミット時に処理される必要があります:

  • 現在のUNDOログが1ページしか占有しておらず、占有しているヘッダーページのサイズがそのサイズの3/4未満しか使用されていない場合、ステータスはUNDOページを再利用できることを示すTRX_UNDO_CACHEDに設定され、新しいUNDOログは現在のUNDOログの後に記録されます。
  • Insert_undoの場合、ステータスはTRX_UNDO_TO_FREEに設定され、アンドゥログを削除することができます!
  • 上記を満たさない場合、アンドゥログはパージスレッドによるクリーンアップ処理を必要とする可能性があることを示します。 状態はTRX_UNDO_TO_PURGEに設定され、アンドゥログはロールバックセグメントのTRX_RSGE_HISTORYに追加され、パージによって回復されます。

purge

InnoDBはMVCCをサポートしているため、トランザクションのコミット時にレコードをすぐに処理することができず、他のトランザクションがこのデータ行を参照している可能性があるためです。

前述したように、ロールバックセグメントであるTRX_RSGE_HISTORY listは、トランザクションがコミットされた順番に従ってアンドゥログをリンクします。

パージ処理中、InnoDBはTRX_RSGE_HISTORYリストからパージが必要な最初のレコードtrx1を見つけます。 パージ後、InnoDBはtrx1があるUNDOログページでパージできるレコードがなくなるまで探し続け、履歴リストに戻ってそして、履歴リストに戻り、次にクリーンアップが必要なレコードを探します。

再利用のため、アンドゥ・ログは異なるトランザクションのアンドゥ・ログを保持することがあり、パージ操作はディスクからの個別の読み込みを含み、遅い処理となります。

MVCC

隠しカラム

ストレージの章で述べたように、行データにはMVCCを実装するために使用される2つの隠しカラムがあります。

TransactionID: DB_TRX_ID。データを操作したトランザクションのトランザクションIDを記録します。

RollPointer: DB_ROLL_PTR、アンドゥ・ログ内のデータの前のバージョンの場所へのポインタ。

トランザクションが行データを変更すると、変更前のデータが元に戻され、TransactionIDが現在のトランザクションIDに、RollPointerが以前のバージョンのデータの場所を指すように変更されます。

例えば、行データのフィールドを A -> B -> C と変更する場合、TransactionID、RollPointer は以下のように変更されます。 注: ここでは、RollPointer を介して、アンドゥログのチェーンに整理されています。

スナップショット

RRレベルでは、スナップショットは、begin/startトランザクションの後のトランザクションの最初のselect読み取り操作の後に作成され、システムで現在アクティブな他のすべてのトランザクションを記録します。

RCレベルでは、スナップショットはトランザクション内の各SELECT文に対して作成されます。

可視性の判定

読み取り対象の行の最後にコミットされたトランザクションIDをtrx_id_currentとします。

現在のトランザクションによって作成されたスナップショットの読み取りビューにおける最も古いトランザクションIDをup_limit_id、最新のトランザクションIDをlow_limit_idとします。

  1. trx_id_current < up_limit_idの場合、現在のトランザクションが行を読み込むと、その行の最新のトランザクションIDがシステム内のすべてのアクティブなトランザクションよりも小さいため、現在の行データが表示されます。
  2. trx_id_current > low_limit_idの場合、現在のトランザクションがオープンされた後に行が変更されコミットされたため、データは表示されません。
  3. up_limit_id <= trx_id_current <= low_limit_id、これは最新のトランザクションがアクティブであることを記録します。

    この時点で、trx_id_currentがスナップショットのアクティブなトランザクションIDのリストにあるかどうかを判断する必要があります。

    そうでない場合、データは表示されます。もしそうでなければ、そのデータは可視です。 もし可視であれば、それは可視ではないので、アンドゥログチェーンを調べて前のバージョンを取得し、それから可視性判定を行う必要があります。

group commit

InnoDBでは、トランザクションコミットは2つの操作を実行します:

  1. メモリ上のトランザクションに対応する情報を変更し、ログをREDOログバッファに書き込みます。
  2. fsyncを呼び出し、REDOログ・バッファをディスクに書き込みます。

グループコミットは、1回のfsyncで複数のトランザクションのREDOログバッファをディスクにフラッシュします。

binlogをオンにすると、InnoDBはストレージエンジンのトランザクションとbinlogの間の一貫性を確保するために、2フェーズトランザクションを使用します。

注意: redoログはinnodbによって生成され、各ページに加えられた変更を記録する物理フォーマットのログで、トランザクションの進行に合わせて常に書き込まれます。

一方、binlogはmysqlの上位レイヤによって生成され、トランザクションのコミットが完了した時に一度だけ書き込まれる論理ログです。

二相トランザクションの手順は以下の通りです。

準備フェーズ: SQLが正常に実行され、REDOおよびUNDOメモリ・ログが生成されます;

binlogコミット・フェーズ:binlogメモリ・ログ・データがファイル・システム・キャッシュに書き込まれ、fsync()を介してディスクに書き込まれます;

コミット・フェーズ: fsync()がbinlogファイル・システム・キャッシュ・ログ・データをディスクに永続的に書き込みます;

リカバリ操作

準備フェーズの前にクラッシュした場合、トランザクションはロールバックされます;

binlog が fsync() されたが InnoDB がコミットしていない時にクラッシュします。リカバリ時に、トランザクション情報が binlog から取得され、再実行され、InnoDB と binlog が常に一致するようにコミットされます。

InnoDB は、ビンログが InnoDB トランザクションのコミット順序と同じ順序で書き込まれるようにする必要があります。InnoDB は最新のトランザクション T3 がコミットされ、リカバリする必要がないことを検出するため、トランザクション T1 のデータが失われるため、リカバリまたはマスター/ベース同期にオンライン・バックアップからのバックアップ・ファイルを使用します。

InnoDBバージョン1.2以前では、prepare_commit_mutexを使用して順序を保証し、前のトランザクションがコミットされた後にロックが解放された場合にのみ、次のトランザクションがprepare操作を実行できました。しかし、これはバイナリロギングがオンになっている場合にグループコミット関数が失敗し、パフォーマンスが低下する原因となっていました。

InnoDBはバージョン1.2から最適化されており

準備フェーズは変更されていません。

binlogコミットフェーズとコミットフェーズは3つのプロセスに分割され、それぞれがキューを保持し、最初にキューに入ったものがリーダースレッド、それ以外がフォロワースレッドとなります。リーダースレッドはフォロワーのトランザクションを収集し、同期を担当し、フォロワースレッドはリーダが操作の完了を通知するのを待ちます。フォロワースレッドは、リーダーが操作の完了を通知するのを待ちます。

  • キュー内のすべてのトランザクションのbinlogをメモリに書き込むフラッシュフェーズ。
  • 同期フェーズでは、メモリキュー内のbinlogは、ディスクにフラッシュされ、キュー内の複数のトランザクションがある場合は、1つだけのfsync操作は、ログの書き込みを完了します。

    コミットフェーズでは、リーダーのInnoDBトランザクションコミットを呼び出すには、キューの順序によると、その後、グループコミット関数を使用することができます。

    すべての3つのフェーズは、キュー順序に従って操作を実行するので、binlogの書き込み順序は、InnoDBトランザクションのコミット順序と一致していることが保証されます。

あるグループがコミットフェイズにある間に、他の新しいグループがフラッシュフェイズに入ることができます。

Read next

Pythonでよく使われるツール正規表現

演習\n\n\nreモジュール\n正規マッチング\nインポート re\ncontent = ''\nre_text = 'akjd\n結果 = re.m

Jun 3, 2020 · 2 min read