MySQLがトランザクション分離を実装する方法の簡単な分析

MySQLがトランザクション分離を実装する方法の簡単な分析

1. はじめに

ご存知のとおり、MySQL の RR 分離レベルでデータをクエリすると、データが他のトランザクションの影響を受けないことを保証できます。ただし、RC 分離レベルでは、他のトランザクションがコミットされている限り、コミット後のデータが読み取られます。では、トランザクション分離の原則は何でしょうか。これはどのように達成されるのでしょうか?これは MVCC メカニズム ( Multi-Version Concurrency Control ) を介して行う必要があります。

注: MySQL の InnoDB エンジンが高性能な同時実行をサポートできる理由は、MySQL の MVCC メカニズム (undo ログ、Read-View などによる) によるものですが、この記事では MVCC については詳しく説明しません。

参考:「MySQL実践45講義」シリーズ。説明は比較的わかりやすいのですが、それでも理解が必要です。例えば、ビュー配列に関する部分は明確に説明されていないと思うので、資料と自分の見解を組み合わせて記録します!

2. RC および RR 分離レベル

RCとRRの分離レベルをそれぞれ開きます。まず、口座テーブルがあると仮定します。トランザクションABCが開始される前、口座の残高は1です。つまり、

select balance from account =1; # 結果は1です

2.1. RRトランザクション分離レベルでのクエリ結果

RRトランザクション分離レベルで3つのトランザクションが開かれると、次の操作が異なる期間に実行されます。

  • トランザクションA(トランザクションを明示的に開き、手動でコミットする):残高を照会する
  • トランザクションB(トランザクションを明示的に開始し、手動でコミット):id=1の残高に1を加算する
  • トランザクション C (トランザクションは明示的に開始されず、自動的にコミットされます): id=1 の残高に 1 を追加します。

時間を論理的に3つの段階に分け、結果を分析します。

  • フェーズ 1: トランザクション A がすぐにトランザクションを開始し、続いてトランザクション B が開始され、その後トランザクション C が残高を 2 に正常に更新します。現在の残高 = 2。
  • フェーズ 2: トランザクション B は、残高の値を更新します。このとき、最初に現在の最新の残高の値を 2 として読み取り、次に残高 = 残高 + 1 を正常に設定し、現在の残高 = 3 になります。
  • フェーズ 3: トランザクション A は、現在 1 である残高の値を照会します (なぜ 1 になるのでしょうか? どのように達成されるのでしょうか? 現在の最新の値 3 であるべきではないのでしょうか? これがこのブログ投稿の焦点です)。最後に、コミットによってトランザクションが終了し、トランザクション B もコミットしてトランザクションを終了します。

最後に、トランザクション A の読み取り残高の結果は 1 です。当然、RR は繰り返し読み取りの略で、つまり、実行中にトランザクションによって表示されるデータは、トランザクションの開始時に表示されたデータと常に一致しています。現在のトランザクションがコミットされているかどうかは、データに影響を与えません。スナップショットに基づいてデータを読み取るだけでよく、これがスナップショット読み取りです。しかし、私たちが議論したいのは、それを MVCC メカニズムの下でどのように実装するかということです。

注意: begin/start transaction コマンドはトランザクションの開始点ではありません。トランザクションは実際には InnoDB テーブルを操作する最初のステートメントが実行された後に開始されます。トランザクションをすぐに開始する場合は、一貫性のあるスナップショットでトランザクションを開始するコマンドを使用できます。

2.2. RCトランザクション分離レベルでのクエリ結果

同様に、RC 分離下でトランザクション ABC を開始し、トランザクション A の最終残高結果を観察します。

最後に、トランザクション A によって読み取られた残高の結果は 2 です。当然、RC は read committable の略で、文字通り、他のトランザクションがコミットされている限り、現在のトランザクションの最新の現在の値をすぐに読み取ることができることを意味します。これが現在の読み取りです。しかし、私たちが議論したいのは、それを MVCC メカニズムの下でどのように実装するかということです。

実際、これは、MVCC の実装で使用される一貫性のある読み取りビューが、RC (Read Committed) および RR (Repeatable Read) 分離レベルの実装をサポートするために使用されるためです。

3. MVCCにおけるトランザクション分離の実装

MVCC がトランザクションの分離を実現する仕組みについて説明する前に、MVCC がトランザクションの分離を実現する仕組みをより深く理解するために、ビュー配列や一貫性のあるビューなどの概念を理解する必要があります。

3.1. データ行の複数のバージョン ROW

InnoDB の各トランザクションには、トランザクション ID と呼ばれる一意のトランザクション ID があります。これは、トランザクションの開始時に InnoDB トランザクション システムに適用され、適用順に厳密に増分されます。

データの各行にも複数のバージョンがあります。トランザクションがデータを更新するたびに、新しいデータ バージョンが生成され、このデータ バージョンのトランザクション ID にトランザクション ID が割り当てられ、行 trx_id として記録されます。同時に、古いデータ バージョンは保持する必要があり、新しいデータ バージョンには、それを直接取得するための情報 (undo_log ファイルを通じて見つかる) が含まれている必要があります。

つまり、データ テーブル内のレコードの行には実際には複数のバージョン (行) があり、各バージョンには独自の行 trx_id があります。

ある時点で 3 つの更新トランザクションが実行されるデータ行 ROW のマルチバージョン管理プロセスについての理解を深めるために、次の図を描いてください。

この図から次のことがわかります。

  • ROW には V1 ~ V4 の 4 つのバージョンがあります。3 回のバランス更新後、最新バージョンは V4 です。現在のバランスは最新の値である 4 に更新されています。
  • InnoDB は、各更新トランザクションによって生成されたトランザクション ID を行 trx_id に割り当てます。
  • undo_log を使用すると、V4 から V1 にロールバックでき、V1 の残高が 1 であることがわかります。これは、undo_log ロールバック バージョンです。

マルチバージョンの原則とデータ行 ROW の実装を理解すると、InnoDB がスナップショットを定義および作成する方法を理解するのに役立ちます。

3.2 ビュー配列

以下の部分は、資料にある原文からの抜粋です。特に赤字部分は理解しにくいかもしれませんので、ご自身の理解と組み合わせて絵を描いてください。

これは、トランザクションの開始時に InnoDB がスナップショットを定義する方法です。どのトランザクション操作を無視でき、どの操作をスナップショットに保存する必要がありますか?これは次のように理解できます。トランザクションは、起動時に「開始時間に基づいて、開始前にデータ バージョンが生成された場合はそれを認識します。開始後に生成された場合はそれを認識せず、以前のバージョンを見つける必要があります」と宣言するだけで済みます。

実装の点では、InnoDB は各トランザクションの配列を構築し、トランザクションが開始された時点で現在「アクティブ」であるすべてのトランザクション ID を保存します。 「アクティブ」とは、開始されているがまだ送信されていないことを意味します。配列内のトランザクション ID の最小値が低水準点として記録され、現在のシステムで作成されたトランザクション ID の最大値に 1 を加えた値が高水準点として記録されます。このビュー配列と最高水準点は、現在のトランザクションの一貫したビュー (読み取りビュー) を構成します。

低水位と高水位についての私の理解:

最低水位標 = 現在開始されているがコミットされていないすべてのトランザクション セットの最小 ID 値 = 現在のトランザクションの前に開始されたがコミットされていない最後のトランザクションの最小 ID 値 (すべてのアクティブなトランザクションの最小 ID 値)

ハイウォーターマーク = 現在のトランザクション ID (現在の ROW バージョン番号 / 行 trx_id) = 作成されたトランザクション ID の最大値 + 1

例えば、上記のRR分離レベルの下の3つのABCトランザクションを例として挙げます。

  • トランザクション A が開始する前は、システム内に ID 99 のアクティブなトランザクションが 1 つだけ存在します。
  • トランザクション A、B、C のバージョン番号はそれぞれ 100、101、102 であり、現在のシステムにはこれら 4 つのトランザクションのみが存在します。
  • 3つのトランザクションが開始される前、データ行(id,balance)=(1,1)の行trx_idは90でした。

したがって、トランザクションAのビュー配列は[99]、トランザクションBのビュー配列は[99,100]、トランザクションCのビュー配列は[99,100,101]となります。つまり、ビュー配列の一般的な式は、[{現在のトランザクションが開かれた時点でのアクティブなトランザクション ID のコレクション}] です。

データ バージョンの可視性ルールは rowtrx_id と一貫性ビューの比較結果に基づいているため、一貫性ビューも理解する必要があります。

3.3. 一貫性ビュー

ビュー配列を理解することで、一貫性のあるビューが容易になります。つまり、このビュー配列とハイ ウォーター マークが現在のトランザクションの一貫性のあるビュー (読み取りビュー) を構成します。

上記のRR分離レベルの下の3つのABCトランザクションを例として挙げます。

  • トランザクションAが開始する前は、システム内にID 99のアクティブなトランザクションが1つだけ存在するため、トランザクションAの開始時点でのアクティブなトランザクションのセットは[99]です。
  • トランザクション A、B、C のバージョン番号はそれぞれ 100、101、102 であり、現在のシステムにはこれらの 4 つのトランザクションしかないため、トランザクション A、B、C の最高水準点はそれぞれ 100、101、102 になります。
  • 3つのトランザクションが開始される前、データ行(id,balance)=(1,1)の行trx_idは90でした。

このように、トランザクション A の一貫性ビューは [99,100]、トランザクション B の一貫性ビューは [99,100,101]、トランザクション C の一貫性ビューは [99,100,101,102] になります。つまり、一貫性のあるビューの一般的な式は、[{現在のトランザクションが開かれた時点でのアクティブなトランザクション ID のセット}、現在の行 trx_id] です。

上記のフローチャートの結果を分析します。

最初の有効な更新バージョンは、残高 = 2 を更新するトランザクション C です。この時点で、最新バージョンの rowtrx_id = 102 ですが、トランザクション ABC の前のアクティブなトランザクションの最新バージョンの rowtrx_id は 99 であるため、この時点で 99 が履歴バージョン 1 になります。

2 番目の有効な更新バージョンは、残高 = 3 を更新するトランザクション B です。この時点で、最新バージョンの rowtrx_id = 101、rowtrx_id = 102 が履歴バージョン 1 になり、rowtrx_id = 99 が履歴バージョン 2 になります。

トランザクションAがクエリを実行すると、トランザクションBは送信されていませんが、生成された(id, balance) = (1, 3)が最新バージョンになります。トランザクションAがデータを読み取ると、一貫性のあるビューは[99, 100]になります。読み取られたデータは現在のバージョンから切り取られ、行trx_idと比較されるため、次の処理が行われます。

  • (1,3)が見つかった場合、行trx_id=101は最高水準点よりも大きく、赤い領域にあり、表示されないことが判断されます。
  • 次に、以前の履歴バージョンを見つけて、行 trx_id=102 を確認します。これは、最高水準点よりも大きく、赤い領域にあるため、表示されません。
  • さらに先を見ていくと、最終的に (1,1) が見つかりました。この行の trx_id は 90 で、低水準点よりも小さく、緑色の領域にあるため、表示されます。

最後に、トランザクションAがいつクエリを実行しても、表示されるデータは、一貫性のあるビュー[99, 100]によって生成されたスナップショットデータ(1, 1)、つまりrowtrx_id=90のときのデータです。これを一貫性のある読み取りと呼びます。

要約:

トランザクション ビューの場合、その更新が常に表示されることに加えて、次の 3 つの状況があります。

  • バージョンは送信されておらず、表示されません。
  • バージョンは送信されましたが、ビューが作成された後に送信されたため表示されません。
  • バージョンは送信されており、ビューが作成される前に表示されます。

ここで、このルールを使用して、図のクエリ結果を判断します。トランザクション A が開始されると、トランザクション A のクエリ ステートメントのビュー配列が生成されます。この時点では、

  • (1,3)はまだ提出されておらず、ケース1に属しており、表示されません。
  • (1,2) 送信されているが、ビュー配列が作成された後に送信されており、ケース2に属し、表示されません。
  • (1,1) はビュー配列が作成されて表示される前に送信されています。

3.4 現在の読み取りとスナップショットの読み取り

3.4.1 現在の読み取りとスナップショットの読み取りルール

もちろん、この一貫性のある読み取りのロジックによれば、トランザクション B はトランザクション C が実質的に balance=2 を更新した後に更新されますが、トランザクション B のビュー配列はトランザクション C で生成されるため、理論的には、トランザクション B はデータ (id、balance)=(1、1) (スナップショット/履歴バージョン) を参照するべきではないでしょうか?現在のバージョン(1、2)のデータは表示されません。残高を更新した直後にトランザクションBのデータが(1, 3)になるのはなぜですか?

トランザクション B が更新前にデータを 1 回選択すると、表示される値は確かに balance=1 になりますが、履歴バージョンで更新を実行することはできません。そうしないと、トランザクション C の更新が失われます。したがって、更新操作では、まず現在のバージョンを読み取ってから更新します。

つまり、データの更新は、まず読み取り、次に更新というルールがあります。読み取りは最新の値を読み取ることであり、これを「現在の読み取り」と呼びます。読み取りを行わずにクエリのみを実行すると、現在のスナップショットが読み取られ、「スナップショット読み取り」と呼ばれます。したがって、トランザクション B は残高を更新する前に、まず最新バージョン (1, 2) をクエリし、次にそれを (1, 3) に更新します。トランザクション A によってクエリされたスナップショット データは (1, 1) であり、最新バージョン (1, 3) ではありません。

3.4.2 現在の読み取りとスナップショット読み取りの説明

現在の読み取り: 共有モードでのロックの選択 (共有ロック)、更新の選択、更新、挿入、削除 (排他ロック) などの操作はすべて現在の読み取りです。つまり、レコードの最新バージョンを読み取ります。読み取り時には、他の同時トランザクションが現在のレコードを変更できないようにする必要があり、読み取られたレコードはロックされます。

スナップショット読み取り: ロック解除された選択操作はスナップショット読み取り、つまりロック解除された非ブロッキング読み取りです。スナップショット読み取りの前提は、分離レベルがシリアル レベルではないことです。シリアル レベルでのスナップショット読み取りは、現在の読み取りに退化します。これはマルチバージョン管理に基づいているため、スナップショットの読み取りでは必ずしも最新バージョンのデータが読み取られるわけではなく、以前の履歴バージョン (スナップショット データ) が読み取られる場合があります。

3.4.3 RC読み取りコミットのルールを表示する

コミットされた読み取りのロジックは、繰り返し可能な読み取りのロジックに似ています。 それらの主な違いは次のとおりです。

繰り返し読み取り分離レベルでは、トランザクションの開始時に一貫性のあるビューを作成するだけで、トランザクション内の他のクエリはこの一貫性のあるビューを共有します。読み取りコミット分離レベルでは、各ステートメントが実行される前に新しいビューが再計算されます。この場合、一貫性のあるスナップショットによるトランザクションの開始は、通常の starttransaction/begin と同等です。したがって、RC 分離レベルでは、トランザクション A とトランザクション B によってクエリされるデータは次のようになります。

トランザクション C は、すぐに残高 = 2 を更新し、自動的にコミットして最新バージョン (1, 2) を生成します。このとき、ビュー データ (1, 2) が再計算されます。トランザクション B は、最新バージョンが (1, 2) であることを見つけ、最新バージョンとしてバージョン (1, 3) に更新します。このときトランザクション B が選択した残高は 1 ではなく 3 です (トランザクション B が残高 = 3 を更新した後、新しいビューがすぐに計算され、このビューに基づいて取得されたデータが選択される)。この時点ではトランザクション B はまだ送信されておらず、トランザクション A からは見えません。そのため、トランザクション A はトランザクション C によって送信された最新バージョンを読み取ります (1、2)。

上記は、MySQL がトランザクション分離を実装する方法の詳細についての簡単な分析です。MySQL トランザクション分離の詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • mysql と oracle のデフォルトのトランザクション分離レベルの説明
  • MySQL トランザクション分離レベルの表示と変更の例
  • Mysql トランザクション分離レベルの読み取りコミットの詳細な説明
  • MySQL の 4 つのトランザクション分離レベルを例を使って分析する
  • MySQL の 4 つのトランザクション分離レベルの詳細な説明
  • MySQLデータベースのトランザクション分離レベルの詳細な説明
  • MySQL の 4 つのトランザクション分離レベルの詳細な説明と比較
  • MySQL トランザクション分離とパフォーマンスへの影響の詳細な分析
  • Innodb トランザクション分離レベルと MySQL のロックの関係に関するチュートリアル
  • MySQL データベースのトランザクション分離レベル (トランザクション分離レベル) の概要

<<:  Vueでクラススタイルを使用する方法の詳細

>>:  Docker を使用した RabbitMQ 環境のデプロイの詳細な紹介

推薦する

Prometheus+Grafanaによるnginxの監視方法を分析する

目次1. ダウンロード2. nginxとnginx-vts-exporterをインストールする3. ...

HTML の順序なしリストタグと順序付きリストタグの使用例

1. 上部と下部のリストタグ: <dl>..</dl>:上dt下層dd: カ...

MySQL 5.7.13 のインストールと設定方法の Mac でのグラフィック チュートリアル

MySQL 5.7.13 Mac用インストールチュートリアル、非常に詳細で、以下のように記録されてい...

jQueryはマウスドラッグ画像機能を実装します

この例では、jQuery を使用してマウス ドラッグ イメージ機能を実装します。まず、ラッパーを設定...

MySQL シリーズ 13 MySQL レプリケーション

目次1. MySQLレプリケーション関連の概念2. シンプルな1マスター1スレーブアーキテクチャの実...

MySQL の繰り返し読み取りレベルでファントム読み取りを解決できますか?

導入データベース理論についてさらに学んでいくうちに、さまざまな分離レベルによって起こり得る問題につい...

ElementuiはデータをxlsxとExcelテーブルにエクスポートします

最近、Vue プロジェクトについて知り、ElementUI でデータを xlsx および Excel...

モバイルレイアウト用の動的REMの実装

ダイナミックレム1. まず、現在の長さの単位を紹介しましょうpx em Mの幅 / 漢字の幅 1em...

MySQL NULLデータ変換方法(必読)

MySQL を使用してデータベースをクエリし、左結合を実行すると、関連付けられたフィールドの一部に...

MySQL パーティションテーブルの制限と制約の詳細な説明

ビルドを無効にするパーティション式では、次の構成はサポートされません。ストアドプロシージャ、ストアド...

MySQL LOAD_FILE() 関数メソッドの概要

MySQL では、LOAD_FILE() 関数はファイルを読み取り、その内容を文字列として返します。...

WeChatアプレットが複数行テキストのスクロール効果を実現

この記事の例では、WeChatアプレットで複数行のテキストスクロールを実装するための具体的なコードを...

js における浅いコピーと深いコピーの詳細な説明

目次1. jsメモリ2. 譲渡3. 浅いコピー4. ディープコピー序文:以下の記事を読む前に、記憶に...

Linux での MySQL のインストールに関する詳細なチュートリアル

1. MySQLサービスをシャットダウンする# service mysqld stop 2. rpm...

MySQL で binlog を使用する際のフォーマットの選択方法

目次1. binlogの3つのモード1.ステートメントレベルモード2. 行レベルモード3. 混合モー...