「まえがき」の記事の内容は、前回に引き続き、大まかにMySQLのトランザクション管理についてです。
「属性列」MySQL
「ホームページリンク」個人ホームページ
《作者》 メイプルリーフ氏(fy)
目次
7. 孤立を再理解する
7.1 データベースの同時実行のシナリオには次のものがあります。
データベースの同時実行には 3 つのシナリオがあります。
- 読み取り/読み取り: 問題はなく、同時実行制御は必要ありません。
- 読み取り/書き込み: スレッドの安全性の問題があり、これによりトランザクション分離の問題が発生する可能性があり、ダーティ読み取り、ファントム読み取り、反復不能読み取りが発生する可能性があります。
- 書き込み-書き込み: スレッドの安全性の問題があり、最初のタイプの更新損失、2 番目のタイプの更新損失などの更新損失の問題が発生する可能性があります。
その中で、読み取り/書き込み同時実行はデータベースで最も頻繁に行われるシナリオです。これについては以下で説明します。読み取り/読み取り同時実行には問題はありません。書き込み/書き込み同時実行については話さないでください。
7.2 マルチバージョン同時実行制御 (MVCC)
マルチバージョン同時実行制御 ( Multi-Version Concurrency Control
) は、读-写冲突
この問題を解決するために使用されるロックフリー同時実行制御であり、主に 3 つの隠しフィールド列、undo
ログ、およびレコード内のRead View
実装に依存します。
MVCC
トランザクションには一方向に増加するトランザクション ID が割り当てられ、変更ごとにバージョンが保存されます。バージョンはトランザクション ID に関連付けられます。読み取り操作では、トランザクションが開始される前にデータベースのスナップショットのみが読み取られます。したがって、MVCC
データベースに関する次の問題は解決できます。
- データベースの読み取りと書き込みを同時に行う場合、読み取り操作中に書き込み操作をブロックすることなく実行でき、書き込み操作が読み取り操作をブロックする必要がないため、データベースの同時読み取りと書き込みのパフォーマンスが向上します。
- 同時に、ダーティ読み取り、ファントム読み取り、非反復読み取りなどのトランザクション分離の問題も解決できますが、更新損失の問題は解決できません。
まず、3 つの隠しフィールド列、undo
ログ、およびRead View
7.3 3 つの隠しフィールド列
データベース テーブルの各レコードには、次の 3 つの隠しフィールド列があります。
DB_TRX_ID
: 6バイト、最後に変更された(変更/挿入された)トランザクションID。このレコードを作成したトランザクションID/このレコードを最後に変更したトランザクションIDを記録します。DB_ROLL_PTR
: 7 バイト、ロールバック ポインター、このレコードの前のバージョンを指します (単純に履歴バージョンを指すと理解され、これらのデータは通常 にありますundo log
)DB_ROW_ID
: 6 バイト、暗黙的な自動インクリメント ID (隠し主キー)。データ テーブルに主キーがない場合、クラスター化インデックスInnoDB
が自動的に生成されます。
DB_ROW_ID
補充する:
- 実際には、削除された非表示フィールド列があり
flag
、データのロールバックを容易にするためにデータが削除されたかどうかを記録するために使用されます (データを削除しても実際には削除されず、フィールドが変更されるだけです)flag
。
たとえば、テストテーブルがあります
create table if not exists student(
name varchar(11) not null,
age int not null
);
データを挿入する
insert into student (name, age) values ('张三', 28);
このテーブルのデータをクエリすると、
見つかったレコードには名前と年齢フィールドだけでなく、3 つの非表示フィールドも含まれています。
説明する:
- レコードを挿入するトランザクションのトランザクション ID が 1 であると仮定すると、
DB_TRX_ID
レコードのフィールドには 1 が埋められます。そうでない場合は、null
- これはテーブルの最初のレコードであるため、暗黙的な主キー
DB_ROW_ID
フィールドには 1 が入力されます。 - レコードは新しく挿入され、履歴バージョンがないため、
DB_ROLL_PTR
ロールバック ポインタの値は次のように設定されます。null
7.4 アンドゥログ
undo log
これは MySQL データベース内のログであり、トランザクションのロールバック情報を記録するために使用されます。- MySQL では、トランザクションのロールバックは次の方法で
undo log
実現されます。
undo log
簡単に理解すると、MySQL のメモリ バッファはログ データの保存に使用され、必要に応じてバッファ内のデータがディスクにフラッシュされます。
7.5 MVCCのシミュレーション
トランザクションID 10のトランザクション
現在、トランザクション ID 10 のトランザクションがあり、学生テーブルに挿入したばかりのレコードの学生名「Zhang San」を「Li Si」に変更するとします。
-
書き込み操作を実行するため、最初に行ロックをレコードに追加する必要があります。
-
変更前に、行変更レコードを にコピーします
undo log
。これにより、undo log
コピーデータの行が存在します(原則はコピーオンライトです)。 -
これで、MySQL には同じレコードの 2 つの行が存在します。
-
ここで、元のレコードの名前を「李思」に変更し、元のレコードの非表示フィールドを
DB_TRX_ID
現在のトランザクションの ID 10 に変更します。デフォルトでは、10 から始まり、その後増加します。 -
元のレコードのロールバック ポインタ
DB_ROLL_PTR
列には、そこに書き込まれたコピー データのアドレスが含まれているundo log
ため、コピー レコードを指します。つまり、以前のバージョンがコピー レコードであることを意味します。 -
最後にトランザクション 10 が送信されるとロックが解除され、この時点での最新のレコードは生徒名「Li Si」のレコードになります。
今、別のトランザクションがあります 11
ここで、student テーブルのレコードを変更 (更新) する別のトランザクション 11 が存在します。change age(28) to age(38)
- 書き込み操作を実行するため、最初にレコード (最新のレコード) に行ロックを追加する必要があります。
- 変更する前に、この行のレコードを にコピーします
undo log
。これで、undo log
にコピー データの別の行が作成されます。 - 次に、元のレコードの生徒の年齢を 38 に変更し、
DB_TRX_ID
レコードの年齢を 11 に変更します。ロールバック ポインタは、コピー先のコピー データのアドレスにDB_ROLL_PTR
設定されるため、レコードの前のバージョンを指します。undo log
- 最後にトランザクション 11 がコミットされるとロックが解除されますが、この時点での最新の記録は学生 38 歳の記録です。
- このようにして、リンクされたリストのレコードに基づいた履歴バージョン チェーンが得られます。
- いわゆるロールバックは、現在のデータを履歴データで上書きすることに他なりません。
上記のバージョンを 1 つずつ、スナップショットと呼びます。
レコードの挿入と削除のバージョン チェーンを維持する方法
更新については上ですでに説明しましたが、更新はバージョン チェーンを形成できますが、挿入と削除はどうなるでしょうか?
- 削除の場合、レコードを削除しても実際にはデータは削除されませんが、最初にレコードのコピーがレコードに配置され、次にレコードの
undo log
削除フラグの非表示フィールドが 1 に設定され、ロールバック後にレコードの削除フラグが非表示になります。フィールドは変更されます。 0 に戻ります。これは、削除されたデータが復元されることと同じです。 - 挿入の場合、新しく挿入されたレコードには履歴バージョンがありませんが、一般にロールバック操作の場合、新しく挿入されたレコードもアンドゥ ログにコピーする必要がありますが、アンドゥ ログにコピーされたレコードはフラグ非表示フィールドを削除します。 1 に設定すると、新しく挿入されたデータはロールバック後に削除されます。
update
バージョンdelete
チェーンを形成できますinsert
select
データには変更が加えられないため、select
複数のバージョンを維持する意味はありません。
「読む」を選択すると、最新バージョンを読むことになりますか? それとも歴史的なバージョンを読みますか?
まず、現在の読み取り値とスナップショットの読み取り値という 2 つの概念について説明します。
- 現在の読み取り: 最新のレコードを読み取ることを現在の読み取りと呼びます。
- スナップショット読み取り: スナップショット読み取りと呼ばれる履歴バージョンを読み取ります。
追加、削除、確認、および変更を行うときに、保護のためにすべてのトランザクションをロックする必要があるわけではありません。
- トランザクションがデータを追加、削除、または変更する場合、トランザクションは最新のレコード、つまり現在の読み取りを操作し、ロック保護が必要です。
- トランザクションが選択クエリを実行するとき、それは現在の読み取りまたはスナップショットの読み取りである可能性があります。現在の読み取りの場合は、ロック保護も必要ですが、スナップショットの読み取りの場合は、履歴が保持されているため、ロックする必要はありません。バージョンが変更されない、つまり、同時に実行できるため、効率が向上します。これが MVCC の意味です
クエリを選択するとき、現在の読み取りとスナップショット読み取りのどちらを実行するかは、分離レベルによって決まります。非コミット読み取りおよびシリアル化された読み取り分離レベルでは、現在の読み取りが実行されますが、コミットされた読み取りと反復読み取り分離レベルでは、両方が実行される場合があります。現在の読み取りまたはスナップショット読み取り。
元に戻すログのバージョン チェーンはいつクリアされますか?
- トランザクションのコミット: トランザクションが正常にコミットされると、データベース システムはトランザクションの操作が永続的であり、ロールバックする必要がないとみなします。したがって、トランザクションに関連付けられたバージョン チェーン
undo log
がクリアされます。 - 他に事情がある場合は特に追加しません。
異なるトランザクションに異なるコンテンツが表示されるようにするにはどうすればよいでしょうか? つまり、分離レベルをどのように実装するかということです。
- 以下で読み取りビューについて説明しましょう。
- Read View は基本的に可視性の判断を行うために使用されます。
- コミットされた読み取りおよび反復可能な読み取り分離レベル (select ステートメント クエリ) での現在の読み取りとスナップショット読み取りのどちらであるべきかの問題を解決します。
7.6 読み取りビュー
Read View
これは、トランザクションの動作快照读
中に生成される読み取りビュー (Read View
)です (つまり、読み取りビューは、select を使用してデータを表示するときに生成されます)。- トランザクション実行のスナップショットが読み取られる瞬間に、データベース システムの現在のスナップショットが生成され、システムの現在アクティブなトランザクションの ID が記録および維持されます (各トランザクションが開始されると、ID が割り当てられます)。この ID は増加するため、最新のトランザクションほど ID 値が大きくなります)
- Read View は MySQL ソースコード内のクラスであり、本来は可視性の判定に使用されます。トランザクションがレコードのスナップショット読み取りを実行すると、そのレコードに対して Read View が作成されます。この Read View に基づいて、可視性の判定が行われます。現在のトランザクションはレコードのデータのバージョンを確認できます
ReadView クラスのソースコードは次のとおりです。
class ReadView {
// 省略...
private:
/** 高水位:大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id;
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
上記4人のメンバーの説明:
m_ids; //一张列表(集合),用来维护Read View生成时刻,系统正活跃的事务ID
m_up_limit_id; //记录m_ids列表中事务ID最小的ID
m_low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
m_creator_trx_id //创建该ReadView的事务ID
トランザクション ID は一方向に増加するため、読み取りビューでのm_up_limit_id
合計に基づいてm_low_limit_id
トランザクション ID を 3 つの部分に分割できます。
- トランザクション ID が以下の
m_up_limit_id
トランザクション m_up_limit_id
との間のトランザクション ID を持つm_low_limit_id
トランザクション- 以上のトランザクション ID を持つ
m_low_limit_id
トランザクション
注記: ReadView はオブジェクトなので、一度初期化されると変化しません (一度のみ初期化されます)。
- それより小さいトランザクション ID を持つ
m_up_limit_id
トランザクションRead View
:生成時に送信されたトランザクションである必要があります。m_up_limit_id
これは、生成時にシステム内でアクティブなトランザクション ID の中で最小の ID であるためRead View
、それより小さいトランザクション ID を持つトランザクションは、読み取りビューの生成時に送信されます。 m_up_limit_id
間のm_low_limit_id
取引IDてRead View
います)m_ids
m_ids
- トランザクション ID 以上の
m_low_limit_id
トランザクションRead View
:生成時にシステムがまだ割り当てていない次のトランザクション ID であるため、生成時に開始されていないトランザクションである必要がありますm_low_limit_id
。Read View
知らせ: トランザクション ID は、10、15、16、17... のように連続している必要はありません。
上記の対応する非表示フィールド:
- 最初の間隔で、
m_creator_trx_id
(トランザクションを作成したトランザクション IDReadView
) ==DB_TRX_ID
またはDB_TRX_ID
<の場合m_up_limit_id
、トランザクションは履歴内でコミットされている (すでにコミットされている) ため、現在のトランザクションで認識される必要があることを意味します。 - 2 番目の間隔がリストに
DB_TRX_ID
ない場合はm_ids
、トランザクションがコミット (コミット) されており、現在のトランザクションで認識される必要があることを意味します。それがリストにある場合はm_ids
、トランザクションと現在のトランザクションの両方がアクティブ (コミットなし) であり、現在のトランザクションによって認識されるべきではないことを意味します。 - 3 番目の間隔
DB_TRX_ID
>= はm_low_limit_id
、トランザクションがスナップショットの後にコミットされ、現在のトランザクションでは認識されないことを示します。
対応するソース コード戦略は次のとおりです。
bool changes_visible(trx_id_t id, const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
//1、事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
check_trx_id_sanity(id, name);
//2、事务id大于等于m_low_limit_id(生成Read View时还没有启动的事务),则不可见
if (id >= m_low_limit_id) {
return(false);
}
//3、事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见
else if (m_ids.empty()) {
return(true);
}
const ids_t::value_type* p = m_ids.data();
//4、事务id位于m_up_limit_id和m_low_limit_id之间,如果在活跃事务id列表中则不可见,如果不在则可见
return (!std::binary_search(p, p + m_ids.size(), id));
}
知らせ: Read View は可視性クラスです。トランザクションの作成時に Read View があるという意味ではありませんが、トランザクション (既に存在) がスナップショット読み取りを実行すると、MySQL は Read View を形成します (選択を実行すると、自動的に形成されます)
7.7 ビュー理論の検証を読む
以下は、Read View の理論的な検証、つまり Read View の全体的なプロセスです。
現在レコードがあり、
そのレコードに対して 4 つのトランザクションが同時に実行されているとします。トランザクション 4 が最初にそれを変更します。トランザクション 4 が変更を完了した後、トランザクション 2 がスナップショットの読み取りを実行します。
- トランザクション 4: 名前 (Zhang San) を名前 (Li Si) に変更します。
事务2
データ行が実行されると快照读
、データベースはデータ行の読み取りビューを生成します。
//事务2的 Read View
m_ids; // 1,3
m_up_limit_id; // 1
m_low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
m_creator_trx_id // 2
現時点のバージョン チェーンは次のとおりです:
トランザクション 4 のみが行レコードを変更し、トランザクション 2 がスナップショット読み取りを実行する前にトランザクションをコミットしました。トランザクション 2 がスナップショット内の行レコードを読み取ると、
行レコードがDB_TRX_ID
続きm_up_limit_id
、m_low_limit_id
アクティブになります。トランザクション ID リスト ( m_ids
) が比較されて、現在のトランザクション 2 が認識できるレコードのバージョンが決定されます。
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
//事务4提交的记录对应的事务ID
DB_TRX_ID=4
//比较步骤
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中
//结论
故,事务4的更改,应该看到
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
8. RRとRCの本質的な違い
RRはRepeatable Readの略で、RCはRead Committedの略です。
以前のクエリ ステートメントはスナップショット読み取りでしたが、現在の読み取りを実行したい場合は、次のステートメントを実行します。
-- 以加共享锁方式进行读取,对应的就是当前读
select * from 表名字 lock in share mode; -- 当前读
実験1
2 つの端末を起動し、分離レベルを反復読み取りに設定し、この時点でテーブル内のデータを確認します。2
つの端末のそれぞれがトランザクションを開始します。左側の端末でのトランザクション操作の前に、右側の端末のトランザクションでトランザクションをチェックさせます。テーブル。左側のターミナルの情報は
テーブル内の情報を変更して送信します。右側のターミナルのトランザクションは変更されたデータを参照できません。左側の
ターミナルはトランザクションを送信します。右側のターミナルは現在の読み取りを実行し、最新のデータを参照できます。
select * from account lock in share mode;
実験2(SQL文の実行順序が異なるだけで同じ操作を実行)
- 2 つの端末を起動し、分離レベルを反復読み取りに設定し、このテーブルのデータを表示します。
- 2 つの端末はそれぞれトランザクションを開始しますが、左側の端末でのトランザクション操作の前に、右側の端末はテーブル内のデータをチェックしません。
左側の端末のトランザクションはテーブル内の情報を変更して送信し、右側の端末のトランザクションにそれを表示させます。このとき、右側の端末のトランザクションは変更されたデータを直接参照します。右側の端末は現在の読み取りを実行します。と、
今読み取ったものが確かに最新のデータであることがわかります。
実験による比較
上記の 2 つの実験の唯一の違いは、左側の端末のトランザクションがデータを変更する前に、右側の端末のトランザクションがスナップショット読み取りを実行したかどうかです。
実験1の操作手順:
実験2の操作手順:
トランザクション B は、トランザクション A が変更される前にスナップショット読み取りを実行しませんでした。
結論は
- RR レベルでは、トランザクション内で毎回読み取られる結果は同じである必要があるため、トランザクションが初めてスナップショット読み取りを実行する場所によって、トランザクションが後続のスナップショット結果を読み取る能力が決まります。
RRとRCの本質的な違い
RC レベルと RR レベルでのスナップショット読み取り結果の違いの原因となるのは、Read View 生成のタイミングの違いです。
- その後、スナップショット読み取りを呼び出すと、同じ
Read View
スナップショット読み取りが引き続き使用されるため、他のトランザクションが更新をコミットする前に現在のトランザクションがスナップショット読み取りを使用している限り
、後続のスナップショット読み取りでは同じスナップショット読み取りが使用されるためRead View
、その後の変更は表示されません。 - つまり、RR レベルでは、
Read View
スナップショット読み取りが生成されると、読み取りビューはその時点で他のすべてのアクティブなトランザクションのスナップショットを記録し、これらのトランザクションの変更は現在のトランザクションには表示されません。 - 作成前に
Read View
トランザクションによって行われたすべての変更が表示されます。 - レベルの下のトランザクションでは
RC
、各スナップショットの読み取りによって新しいスナップショットの合計が生成されるRead View
ため、RC
そのレベルの下のトランザクションで他のトランザクションによって送信された更新を確認できます。 - つまり、
RC
分離レベルでは、各スナップショットの読み取りにより最新のスナップショットが生成され、取得されます。Read View
- RR 分離レベルでは、同じトランザクション内の最初のスナップショット読み取りのみが作成され
Read View
、後続のスナップショット読み取りでは同じものが取得されます。Read View
- スナップショットが読み取られるたびに形成されるのはまさに RC であるため
Read View
、RC
反復不可能な読み取りの問題が発生します。
- - - - - - - - - - - 終わり - - - - - - - - - - -
「 作者 」 枫叶先生
「 更新 」 2023.9.10
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。