MySQLトランザクション

1. ビジネスの基礎知識

1.1 データベーストランザクションの概要

1.1.1 ストレージ エンジンのサポート

SHOW ENGINES コマンドを使用して、現在 MySQL でサポートされているストレージ エンジンと、それらのストレージ エンジンがトランザクションをサポートしているかどうかを確認します。

MySQL では、InnoDB のみがトランザクションをサポートしていることがわかります。

1.1.2 基本概念

トランザクション: データをある状態から別の状態に変換する一連の論理的な操作単位。

トランザクション処理の原則:

すべてのトランザクションが作業単位として実行されることが保証されており、障害が発生した場合でも、この実行方法を変更することはできません。

1 つのトランザクションで複数の操作が実行される場合、すべてのトランザクションが送信 (コミット) され、これらの変更が永続的に保存されるか、データベース管理システムによって行われたすべての変更が破棄され、トランザクション全体が初期状態にロールバック (ロールバック) されます。

1.1.3 トランザクションの ACID 特性

原子性

アトミック性とは、トランザクションが分割できない作業単位であり、すべてが送信されるか、すべてが失敗してロールバックされるかのいずれかであることを意味します。

一貫性

国内の多くのサイトで整合性の記述に誤りがありますので、詳しくはWikipediaの整合性の説明を参照してください。

定義上、一貫性とは、トランザクションの実行前後における、ある法的状態から別の法的状態へのデータの変換を指します。この状態は文法的なものではなく意味的なものであり、特定のビジネスに関連しています。

では、法的なデータ状態とは何でしょうか? あらかじめ決められた制約を満たす状態を法的状態といいます。平たく言えば、この状態はそれ自体で定義されます(現実世界の制約を満たすなど)。この状態が満たされている場合、データは一貫しています。この状態が満たされていない場合、データは不一致です。トランザクション内の操作が失敗した場合、システムは現在実行中のトランザクションを自動的にキャンセルし、トランザクション操作前の状態に戻ります。

隔離

トランザクション分離とは、トランザクションの実行が他のトランザクションによって干渉されないこと、つまり、トランザクション内で使用される操作とデータが他の同時トランザクションから分離され、同時に実行されるトランザクションが互いに干渉できないことを意味します。

耐久性

永続性とは、トランザクションがコミットされると、データベース内のデータへの変更が永続的になり、その後の他の操作やデータベース障害がトランザクションに影響を与えないことを意味します。

永続性はトランザクション ログによって保証されます。ログには、REDO ログとロールバック ログが含まれます。トランザクションを通じてデータを変更する場合、まずデータベースの変更情報をREDOログに記録し、次にデータベース内の対応する行を変更します。この利点は、データベース システムがクラッシュした場合でも、データベースの再起動後にデータベース システム内で更新されていない REDO ログを見つけて再実行できるため、トランザクションが永続化されることです。

1.1.4 取引ステータス

トランザクションは抽象的な概念であり、実際には 1 つ以上のデータベース操作に対応することがわかりました。MySQL では、これらの操作のさまざまな段階に従ってトランザクションをいくつかの状態に大まかに分割します。

  • アクティブ

トランザクションに対応するデータベース操作が実行されているとき、トランザクションはアクティブ状態にあると言います。

  • 部分的にコミット済み

トランザクション内の最後の操作が実行されたが、操作がメモリ内で実行されるため、その影響がディスクにフラッシュされない場合、トランザクションは部分的にコミットされた状態にあると言います。

  • 失敗した

トランザクションがアクティブまたは部分的にコミットされた状態にある場合、何らかのエラー (データベース自体のエラー、オペレーティング システムのエラー、直接的な電源障害など) が発生し、実行を続行できなくなったり、現在のトランザクションの実行を人為的に停止したりする場合があります。 we will トランザクションが失敗した状態であることを示します。

  • 中止された

トランザクションが部分的に実行されて失敗状態になった場合、変更されたトランザクションの操作をトランザクションが実行される前の状態に復元する必要があります。言い換えれば、失敗したトランザクションが現在のデータベースに与えた影響を元に戻すことです。この元に戻すプロセスをロールバックと呼びます。ロールバック操作が完了したとき、つまりデータベースがトランザクション実行前の状態に復元されたとき、トランザクションは中止された状態にあると言います。

  • 関与する

部分的にコミットされた状態にあるトランザクションが、変更されたすべてのデータをディスクに同期する場合、そのトランザクションはコミットされた状態にあると言えます。

基本的な状態遷移図は次のようになります。

1.2 トランザクションの使い方

トランザクションを使用するには、明示的トランザクションと暗黙的トランザクションの 2 つの方法があります。

1.2.1 明示的なトランザクション

ステップ 1: START TRANSACTION または BEGIN を使用して、トランザクションを明示的に開きます。

mysql> BEGIN; 
#或者 
mysql> START TRANSACTION;

BEGIN と比較すると、START TRANSACTION ステートメントは、その後にいくつかの修飾子を続けることができるという点で特殊です。

  • READ ONLY: 現在のトランザクションが読み取り専用トランザクションであることを示します。つまり、トランザクションに属するデータベース操作はデータの読み取りのみが可能ですが、データの変更はできません。
  • READ WRITE : 現在のトランザクションが読み取り/書き込みトランザクションであることを示します。つまり、トランザクションに属するデータベース操作はデータの読み取りまたはデータの変更のいずれかを行うことができます。
  • 一貫したスナップショットを使用: 一貫した読み取りを開始します。

ステップ2:一連のトランザクションでの操作(主にDML、DDLは除く)

ステップ 3: トランザクションをコミットするか、トランザクションを中止します (つまり、トランザクションをロールバックします)。

# 提交事务。当提交事务后,对数据库的修改是永久性的。 
mysql> COMMIT;

# 回滚事务。即撤销正在进行的所有没有提交的修改 
mysql> ROLLBACK; 
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]

1.2.2 暗黙的なトランザクション

MySQL にはシステム変数 autocommit があります。

mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)

もちろん、この自動送信機能をオフにしたい場合は、次の 2 つの方法のいずれかを使用できます。

  • START TRANSACTION または BEGIN ステートメントを使用してトランザクションを明示的に開始します。このようにして、トランザクションがコミットまたはロールバックされる前に、自動コミット機能が一時的にオフになります。
  • 次のように、システム変数 autocommit の値を OFF に設定します。
SET autocommit = OFF;
#或
SET autocommit = 0;

1.2.3 データが暗黙的に送信される場合

  • データ定義言語(データ定義言語、略称:DDL)
  • mysql データベース内のテーブルを暗黙的に使用または変更する
  • トランザクション制御またはロックに関するステートメント
    • トランザクションがコミットまたはロールバックされる前に、START TRANSACTION または BEGIN ステートメントを使用して別のトランザクションが開始されると、前のトランザクションは暗黙的にコミットされます。たった今
    • autocommit システム変数の現在の値は OFF です。これを手動で ON にすると、前のステートメントが属するトランザクションも暗黙的にコミットされます。
    • LOCK TABLES、UNLO​​CK TABLES、およびロックに関するその他のステートメントを使用すると、前のステートメントが属するトランザクションも暗黙的にコミットされます。
  • データをロードするステートメント
  • MySQL レプリケーションに関するいくつかのステートメント

1.2.4 コミットとロールバック

MySQL のデフォルト状態、次のトランザクションの最終的な処理結果を見てください。

ケース 1:

CREATE TABLE user(name varchar(20), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
BEGIN;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
SELECT * FROM user;

実行結果 (1 行のデータ):

mysql> commit;
Query OK, 0 rows affected (0.00 秒)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 秒)
mysql> INSERT INTO user SELECT '李四';
Query OK, 1 rows affected (0.00 秒)
mysql> INSERT INTO user SELECT '李四';
Duplicate entry '李四' for key 'user.PRIMARY'
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 秒)
mysql> select * from user;
+--------+
| name |
+--------+
| 张三 |
+--------+
1 行于数据集 (0.01 秒)

ケース 2:

CREATE TABLE user (name varchar(20), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
mysql> SELECT * FROM user;
+--------+
| name |
+--------+
| 张三 |
| 李四 |
+--------+
2 行于数据集 (0.01 秒)

autocommit=0 が設定されている場合、トランザクションの開始に START TRANSACTION または BEGIN のどちらが使用されたかに関係なく、トランザクションを有効にするには COMMIT が必要で、トランザクションをロールバックするには ROLLBACK が使用されます。

autocommit=1 が設定されている場合、各 SQL ステートメントは自動的に送信されます。ただし、現時点では、STARTTRANSACTION または BEGIN を使用してトランザクションを明示的に開くと、トランザクションは COMMIT の場合にのみ有効になり、ROLLBACK の場合はロールバックされます。

1.3 トランザクション分離レベル

MySQL はクライアント/サーバー アーキテクチャのソフトウェアであり、同じサーバーに対して複数のクライアントが接続でき、各クライアントがサーバーに接続された後のセッション (セッション) と呼ばれます。

各クライアントは、独自のセッションでリクエスト ステートメントをサーバーに送信できます。リクエスト ステートメントは特定のトランザクションの一部である場合があります。つまり、サーバーでは複数のトランザクションが同時に処理される場合があります。

トランザクションには分離の特性があります。理論的には、トランザクションが特定のデータにアクセスする場合、他のトランザクションはキューに入れられる必要があります。トランザクションがコミットされた後、他のトランザクションは引き続きデータにアクセスできます。しかし、これはパフォーマンスにあまりにも大きな影響を与えます。トランザクションの分離を維持する必要があり、同じデータにアクセスする複数のトランザクションを処理するときにサーバーのパフォーマンスをできるだけ高くしたいと考えています。それは 2 つの間のトレードオフによって決まります。

1.3.1 データの準備

テーブルを作成するには:

CREATE TABLE student (
studentno INT,
name VARCHAR(20),
class varchar(20),
PRIMARY KEY (studentno)
) Engine=InnoDB CHARSET=utf8;

次に、このテーブルにデータを挿入します。

INSERT INTO student VALUES(1, '小谷', '1班');

1.3.2 データの同時実行性の問題

トランザクションの分離と同時実行については、どのように選択すればよいでしょうか? まず、同じデータにアクセスするトランザクションが連続して実行される (つまり、一方が実行されてからもう一方が実行される) ことが保証されていない場合に発生する可能性がある問題を見てみましょう。

1. 脏写(ダーティ・ライト)

2 つのトランザクション セッション A とセッション B の場合、コミットされていない別のトランザクション セッション B によって変更されたデータをトランザクション セッション A が変更した場合、ダーティ ライトが発生したことを意味します。

2. ダーティリード

セッション A とセッション B の 2 つのトランザクションの場合、セッション A はセッション B によって更新されたがまだコミットされていないフィールドを読み取ります。後でセッション B がロールバックした場合、セッション A によって読み取られたコンテンツは一時的なものとなり、無効になります。

セッション A とセッション B がそれぞれトランザクションを開始します。セッション B のトランザクションは、最初に学生番号が 1 であるレコードの名前列を「Zhang San」に更新し、次にセッション A のトランザクションは学生番号が 1 であるレコードをクエリします。カラム名の値が「Zhang San」として読み取られ、セッション B のトランザクションが後でロールバックされると、セッション A のトランザクションは存在しないデータを読み取ることと同じになり、この現象はダーティ リードと呼ばれます。

3. ノンリピータブルリード(Non-Repeatable Read)

セッション A、セッション B の 2 つのトランザクションの場合、セッション A がフィールドを読み取り、次にセッション B がフィールドを更新します。セッション A が同じフィールドを再度読み取ると、値は異なります。これは、反復不可能な読み取りが発生したことを意味します。

セッション B でいくつかの暗黙的なトランザクションを送信しました (これらは暗黙的なトランザクションであることに注意してください。つまり、トランザクションはステートメントの終了時に送信されます)。これらのトランザクションは、studentno 列が 1 であるレコードの列名の値を変更しました。その後、各トランザクションが送信され、セッション A のすべてのトランザクションが最新の値を表示できる場合、この現象は非反復読み取りとも呼ばれます。

4.ファントムリーディング(ファントム)

セッション A、セッション B の 2 つのトランザクションの場合、セッション A がテーブルからフィールドを読み取り、次にセッション B がテーブルに新しい行を挿入します。その後、セッション A が同じテーブルを再度読み取ると、さらにいくつかの行が存在します。これはファントムリードが発生したことを意味します。

セッション A のトランザクションは、まず、条件 Studentno > 0 に従ってテーブル Student をクエリし、name 列の値が「Zhang San」であるレコードを取得します。次に、暗黙的なトランザクションがセッション B に送信され、テーブル Student にエントリが挿入されます。新しいレコード。その後、セッション A のトランザクションは同じ条件 Studentno > 0 に従ってテーブル Student をクエリし、結果セットにはセッション B のトランザクションによって新しく挿入されたレコードが含まれます。この現象はファントム読み取りとも呼ばれます。新しく挿入されたレコードをファントム レコードと呼びます。

1.3.3 SQL の 4 つの分離ドメイン

上記では、複数のトランザクションを同時に実行するときに発生する可能性のあるいくつかの問題を紹介しました。これらの問題には優先順位が付けられています。これらの問題を重大度に従ってランク付けします。

ダーティ ライト > ダーティ リード > ノンリピータブル リード > ファントム リード

ここでは、パフォーマンスの一部と引き換えに分離の一部を犠牲にすることができます。いくつかの分離レベルを設定します。分離レベルが低いほど、同時実行の問題が発生します。SQL 標準では 4 つの分離レベルが確立されています。

  • コミットされていない読み取り

コミットされていない読み取り。この分離レベルでは、すべてのトランザクションが他のコミットされていないトランザクションの実行結果を確認できます。ダーティ リード、反復不能リード、ファントム リードは回避できません。

  • コミットされた読み取り

コミットされた読み取り。これは分離の単純な定義を満たします。つまり、トランザクションはコミットされたトランザクションによって行われた変更のみを確認できます。これは、ほとんどのデータベース システムのデフォルトの分離レベルです (ただし、MySQL では異なります)。ダーティ リードは回避できますが、ノンリピータブル リードとファントム リードの問題は依然として存在します。

  • 反復読み取り

反復可能な読み取り。トランザクション A がデータを読み取った後、トランザクション B がその時点でデータを変更して送信し、その後トランザクション A がデータを再度読み取ります。元のコンテンツは引き続き読み取られます。ダーティ リードと非反復読み取りは回避できますが、ファントム リードは依然として存在します。これは、MySQL のデフォルトの分離レベルです。

  • シリアル化可能

直列化可能性。これにより、トランザクションがテーブルから同じ行を読み取ることができます。このトランザクションの実行中、他のトランザクションはテーブルに対して挿入、更新、および削除の操作を実行できません。同時実行の問題はすべて回避できますが、パフォーマンスは非常に低下します。ダーティ リード、非反復読み取り、ファントム リードを回避できます。

SQL 標準によれば、分離レベルが異なると、同時トランザクションで異なる重大度の問題が発生する可能性があります。詳細は次のとおりです。

なぜ汚い書き込みは関係ないのでしょうか?ダーティ ライティングの問題は深刻すぎるため、どのような分離レベルであっても、ダーティ ライティングは発生することが許可されません。

分離レベルが異なると現象も異なり、ロックと同時実行メカニズムも異なります。分離レベルが高くなるほど、データベースの同時実行パフォーマンスは低下します。4 つのトランザクション分離レベルと同時実行パフォーマンスの関係は次のとおりです。

1.3.4 Mysql でサポートされる 4 つの分離レベル

MySQL のデフォルトの分離レベルは REPEATABLE READ ですが、トランザクションの分離レベルは手動で変更できます。

# 查看隔离级别,MySQL 5.7.20的版本之前:
mysql> SHOW VARIABLES LIKE 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

# MySQL 5.7.20版本之后,引入transaction_isolation来替换tx_isolation
# 查看隔离级别,MySQL 5.7.20的版本及之后:
mysql>  SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 1 warning (0.00 sec)

#或者不同MySQL版本中都可以使用的:
SELECT @@transaction_isolation;

1.3.5 トランザクションの分離ドメインの設定方法

次のステートメントにより、トランザクションの分離レベルを変更します。

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
SET GLOBAL TRANSACTION_ISOLATION = 隔离级别;
#其中,隔离级别格式:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE

# 举例
mysql> set session TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-COMMITTED          |
+-------------------------+
1 row in set (0.00 sec)

設定時に GLOBAL または SESSION を使用した場合の影響について:

  • GLOBAL キーワードを使用します (グローバルに影響します)。
    • 現在存在するセッションは無効です
    • ステートメントの実行後に生成されたセッションでのみ機能します
  • SESSION キーワードを使用します (セッション スコープに影響します)。
    • 現在のセッションの後続のすべてのトランザクションに有効です
    • トランザクション間で実行された場合、後続のトランザクションでも有効です
    • このステートメントは、すでに開かれているトランザクションの途中でも実行できますが、現在実行中のトランザクションには影響しません。

1.3.6 さまざまな分離レベルの例

例: アカウント テーブルがあり、ユーザー 1 の残高は 100、ユーザー 2 の残高は 0 です。

デモ 1. コミットされていないダーティ リードの読み取り

分離レベルを非コミットを読み取るように設定します。

トランザクション1とトランザクション2の実行フローは以下のとおりです。

デモ 2: コミットされた読み取り

デモ 3: 反復読み取り

分離レベルを反復読み取りに設定すると、トランザクションの実行フローは次のようになります。

デモ 4: ファントム リーディング

デモ 5: シーケンシャル

時間

トランザクション1

トランザクション2

T1

セッショントランザクション分離レベルをシリアル化可能に設定します。

トランザクションを開始します。

select count(*) from account where id = 4; #結果は 0

T2

セッショントランザクション分離レベルをシリアル化可能に設定します。

トランザクションを開始します。

insert into account(id, account) value (4,100); #常に送信できないため、トランザクション 1 が送信されるまで待つ必要があります

T3

専念;

T4

Insert ステートメントはライブラリにのみ保存できます。

T5

select count(*) from account where id = 4; #結果はまだ 0

T6

専念;

T7

select count(*) from account where id = 4; #結果は 1

1.4 取引の一般的な分類

トランザクション理論の観点から、トランザクションは次のタイプに分類できます。

  • フラットトランザクション
  • セーブポイントを使用したフラット トランザクション
  • 連鎖トランザクション
  • ネストされたトランザクション
  • 分散トランザクション

2、Mysql トランザクション ログ

トランザクションには、原子性、一貫性、分離性、耐久性という 4 つの特性があります。

では、取引の4つの特徴はどのような仕組みに基づいているのでしょうか?

  • トランザクションの分離はロック機構によって実現されます。
  • トランザクションのアトミック性、一貫性、耐久性は、トランザクションの REDO ログと UNDO ログによって保証されます。
    • REDO LOGはREDOログと呼ばれ、書き換え操作を提供し、コミットされたトランザクションによって変更されたページ操作を復元して、トランザクションの耐久性を確保します。
    • UNDO LOG はロールバック ログと呼ばれ、トランザクションの原子性と一貫性を確保するために、ロールバック行は特定のバージョンに記録されます。

DBA の中には、UNDO が REDO の逆のプロセスであると考えている人もいますが、そうではありません。

2.1 やり直しログ

2.1.1 REDO ログが必要な理由

一方で、バッファ プールは CPU とディスク間のギャップをなくすのに役立ち、チェックポイント メカニズムはデータの最終配置を保証しますが、チェックポイントは変更されるたびにトリガーされるわけではないため、処理されます。の間隔でマスタースレッドによって実行されます。したがって、最悪のケースは、トランザクションがコミットされた後、バッファー プールに書き込まれたばかりで、データベースがダウンすると、このデータが失われ、回復できなくなることです。

一方、トランザクションには永続性の特性が含まれています。つまり、コミットされたトランザクションの場合、トランザクションがコミットされた後にシステムがクラッシュしたとしても、このトランザクションによってデータベースに加えられた変更は失われることはありません。

では、この永続性を確保するにはどうすればよいでしょうか? シンプルなアプローチ: トランザクションがコミットされる前に、トランザクションによって変更されたすべてのページをディスクにフラッシュします。ただし、このシンプルで粗雑なアプローチにはいくつかの問題があります。

別の解決策: コミットされたトランザクションによってデータベース内のデータに加えられた変更を永続的にしたいだけで、後でシステムがクラッシュした場合でも、再起動後に変更を復元できます。したがって、実際には、トランザクションがコミットされるたびに、メモリ内のトランザクションによって変更されたすべてのページをディスクにフラッシュする必要はなく、変更された内容を記録するだけで十分です。たとえば、トランザクションは、システム表領域のページ 10 のオフセット 100 にあるバイトの値 1 を 2 に変更します。必要なのは、テーブルスペース 0 のページ 10 のオフセット 100 の値を 2 に更新するという記録だけです。

2.1.2 REDO ログの利点と特徴

  • 利点
    • REDO ログによりフラッシュの頻度が減少します。
    • REDO ログが占有するスペースはほとんどありません
  • 特徴
    • REDO ログはディスクに順番に書き込まれます
    • トランザクションの実行中、REDO ログは記録を続けます。

2.1.3 REDOの構成

REDO ログは単純に次の 2 つの部分に分けることができます。

  • メモリに保存される REDO ログ バッファ (REDO ログ バッファ) は揮発性です。
    • パラメータ設定: innodb_log_buffer_size: REDO ログ バッファ サイズ、デフォルトは 16M、最大値は 4096M、最小値は 1M です。
mysql> show variables like '%innodb_log_buffer_size%';
+------------------------+---------+
| Variable_name          | Value   |
+------------------------+---------+
| innodb_log_buffer_size | 1048576 |
+------------------------+---------+
1 row in set, 1 warning (0.17 sec)
  • ハードディスクに保存される REDO ログ ファイル (REDO ログ ファイル) は永続的です。

2.1.4 やり直しの全体的なプロセス

更新トランザクションを例として、REDO ログのフロー プロセスを次の図に示します。

  1. まず元のデータをディスクからメモリに読み取り、データのメモリ コピーを変更します。
  2. REDO ログを生成し、REDO ログ バッファに書き込み、データの変更された値を記録します。
  3. トランザクションがコミットされると、REDO ログ バッファ内の内容が REDO ログ ファイルに更新され、REDO ログ ファイルが write メソッドに追加されます。
  4. メモリ内の変更されたデータを定期的にディスクにフラッシュします

先行書き込みログ (ログ前の永続化): データ ページを永続化する前に、まず対応するログ ページをメモリ内に永続化します。

2.1.5 REDO ログのフラッシュ戦略

REDO ログの書き込みはディスクに直接書き込まれず、InnoDB エンジンは REDO ログを書き込むときにまず REDO ログ バッファに書き込み、その後一定の頻度で実際の REDO ログ ファイルにフラッシュします。

ここで特定の周波数についてはどうでしょうか?これがブラッシング戦略について言いたいことです。

REDO ログ バッファを REDO ログ ファイルにフラッシュするプロセスは、実際にはディスクにフラッシュされるのではなく、ファイル システム キャッシュ (ページ キャッシュ) にフラッシュされるだけであることに注意してください (これは、ファイルの書き込み効率を向上させるために最新のオペレーティング システムによって行われます)。 )、実際の書き込みはシステムの判断に委ねられます (たとえば、ページ キャッシュが十分に大きいなど)。次に、InnoDB の問題があり、同期のためにシステムに引き渡されると、システムがダウンした場合にデータも失われます (システム全体がダウンする可能性はまだ比較的小さいですが)。

この状況に対応して、InnoDB は innodb_flush_log_at_trx_commit パラメータを提供します。これは、コミットがトランザクションをコミットするときに REDO ログ バッファ内のログを REDO ログ ファイルにフラッシュする方法を制御します。次の 3 つの戦略がサポートされています。

  • 0 に設定すると、トランザクションが送信されるたびにディスク操作が実行されないことを意味します。(システムのデフォルトでは、マスタースレッドは 1 秒ごとに REDO ログを同期します)
  • 1 に設定: トランザクションがコミットされるたびに同期が行われ、ディスク操作が実行されることを意味します (デフォルト値)
  • 2 に設定すると、トランザクションがコミットされるたびに REDO ログ バッファの内容のみがページ キャッシュに書き込まれ、同期は実行されません。ディスク ファイルといつ同期するかは OS によって決定されます。

2.1.6 さまざまなブラッシング戦略のデモンストレーション

2.1.7 REDOログバッファ書き込み処理

1. 補足概念: ミニトランザクション

トランザクションには複数のステートメントを含めることができます。各ステートメントは実際には複数の mtr で構成され、各 mtr には複数の REDO ログを含めることができます。それらの関係を示す図を次のように描きます。

2. REDO ログがログ バッファに書き込まれる

各 MTR は REDO ログのセットを生成し、模式図を使用してこれらの MTR によって生成されるログの状況を説明します。

異なるトランザクションが同時に実行される可能性があるため、T1 と T2 の間の mtr は交互に実行される場合があります。

3. REDOログブロックの構造図

 2.1.8 REDO ログバッファ

1. 関連パラメータの設定

  • innodb_log_group_home_dir : REDO ログ ファイル グループが配置されるパスを指定します。デフォルト値は ./ で、データベースのデータ ディレクトリにあることを意味します。MySQL のデフォルトのデータ ディレクトリ (var/lib/mysql) には、デフォルトで ib_logfile0 および ib_logfile1 という名前の 2 つのファイルがあり、ログ バッファ内のログはデフォルトでこれら 2 つのディスク ファイルにフラッシュされます。この REDO ログ ファイルの場所は変更することもできます。
  • innodb_log_files_in_group: ib_logfile0、iblogfile1...iblogfilen という名前の REDO ログ ファイルの数を指定します。デフォルトは 2 で、最大値は 100 です。
mysql> show variables like 'innodb_log_files_in_group';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
#ib_logfile0
#ib_logfile1
  • innodb_flush_log_at_trx_commit: REDO ログのディスクへのフラッシュを制御するポリシー。デフォルトは 1 です。
  • innodb_log_file_size: 単一 REDO ログ ファイルのサイズを設定します。デフォルト値は 48M です。最大値は 512G です。最大値は REDO ログ シリーズ ファイル全体の合計を指すことに注意してください。つまり、(innodb_log_files_in_group * innodb_log_file_size) は最大値 512G を超えることはできません。
mysql> show variables like 'innodb_log_file_size';
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+

より大規模なトランザクションに対応できるように、ビジネスに応じてサイズを変更します。以下に示すように、my.cnf ファイルを編集し、データベースを再起動して有効にします。

[root@localhost ~]# vim /etc/my.cnf
innodb_log_file_size=200M

2. ログファイルグループ

実際の REDO ログ ファイルの合計サイズは、innodb_log_file_size × innodb_log_files_in_groupです。

データが REDO ログ ファイル グループに循環的に書き込まれる場合、後で書き込まれた REDO ログは、先に書き込まれた REDO ログを上書きしますか?

そうです!そこで、InnoDB の設計者はチェックポイントの概念を提案しました。

3. チェックポイント

書き込み pos がチェックポイントに追いついた場合は、ログ ファイル グループがいっぱいで、現時点では新しい REDO ログ レコードを書き込むことができないことを意味します。MySQL は停止し、いくつかのレコードをクリアして、チェックポイントを進める必要があります。

2.2 アンドゥログ

REDO ログはトランザクションの永続性を保証し、UNDO ログはトランザクションのアトミック性を保証します。トランザクションでデータを更新する前操作は、実際には最初に元に戻すログを書き込むことです。

2.2.1 アンドゥログの見方

トランザクションはアトミック性、つまりトランザクション内の操作が完了するか、何も行われないかを保証する必要があります。ただし、トランザクションの実行中に次のような状況が発生する場合があります。

  • 状況 1: トランザクションの実行中に、サーバー自体のエラー、オペレーティング システムのエラー、さらには突然の停電によるエラーなど、さまざまなエラーが発生する可能性があります。
  • ケース 2: プログラマは、トランザクションの実行中に ROLLBACK ステートメントを手動で入力して、現在のトランザクションの実行を終了できます。

上記の状況が発生した場合、データを元の状態に戻す必要があります。このプロセスはロールバックと呼ばれ、このトランザクションは何も実行していないように見えるため、アトミック性の要件を満たしているという誤った印象を与える可能性があります。

2.2.2 アンドゥログの役割

  • 機能 1: データのロールバック
  • 役割 2: MVCC

2.2.3 アンドゥの格納構造

1. セグメントのロールバックとページの取り消し

InnoDB は、ロールバック セグメントである UNDO ログ管理にセグメント化されたアプローチを使用します。

各ロールバック セグメントは 1024 個の UNDO ログ セグメントを記録し、各 UNDO ログ セグメント内の UNDO ページに適用されます。

  • InnoDB1.1 より前のバージョン (1.1 バージョンを除く) では、ロールバック セグメントが 1 つしかないため、同時にサポートされるトランザクション制限は 1024 です。ただし、ほとんどのアプリケーションにはこれで十分です。
  • バージョン 1.1 以降、InnoDB は最大 128 個のロールバック セグメントをサポートするため、同時オンライン トランザクションの制限は 128*1024 に増加しました。
mysql> show variables like '%undo%';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
| innodb_undo_directory    | .\         |
| innodb_undo_log_truncate | OFF        |
| innodb_undo_logs         | 128        |
| innodb_undo_tablespaces  | 0          |
+--------------------------+------------+
5 rows in set, 1 warning (0.00 sec)

2. セグメントとトランザクションのロールバック

  1. 各トランザクションは 1 つのロールバック セグメントのみを使用し、1 つのロールバック セグメントは同時に複数のトランザクションを処理できます。
  2. トランザクションが開始されると、ロールバック セグメントが作成され、トランザクション中にデータが変更されると、元のデータがロールバック セグメントにコピーされます。
  3. ロールバック セグメントでは、トランザクションが終了するかすべての領域が使い果たされるまで、トランザクションはエクステントを埋め続けます。現在のエクステントが十分でない場合、トランザクションはセグメント内の次のエクステントの拡張を要求します。割り当てられたエクステントがすべて使い果たされた場合、トランザクションは元のエクステントを上書きするか、ロールバック セグメントが許可している場合は新しいエクステントを拡張します。使用するパネル。
  4. ロールバック・セグメントはUNDO表領域に存在します。データベースには複数のUNDO表領域が存在できますが、一度に使用できるUNDO表領域は1つだけです。
  5. トランザクションがコミットされると、InnoDB ストレージ エンジンは次の 2 つのことを実行します。
    1. 後でパージ操作を行うために、元に戻すログをリストに追加します。
    2. UNDO ログが存在するページが再利用できるかどうか、次のトランザクションに割り当てることができるかどうかを判断します。

3. ロールバックセグメントでのデータ分類

  • コミットされていない取り消し情報
  • コミットされているが期限切れになっていないロールバック データ (コミットされた取り消し情報)
  • トランザクションにはコミットされ期限切れのデータがあります (期限切れの元に戻す情報)

2.2.4 アンドゥの種類

InnoDB ストレージ エンジンでは、UNDO ログは次のように分割されます。

  • アンドゥログを挿入
  • アンドゥログを更新する

2.2.5 アンドゥログのライフサイクル

1. 簡単な生成プロセス

バッファプールの処理のみ:

REDO ログと UNDO ログを使用する場合:

2. 詳細な生成プロセス

INSERT を実行すると、次のようになります。

begin; 
INSERT INTO user (name) VALUES ("tom");

UPDATE を行うとき:

UPDATE user SET name='Sun'' WHERE id=1;

UPDATE ユーザー SET id=2 WHERE id=1;

3. アンドゥログのロールバック方法

上記の例を例に挙げると、ロールバックが実行されると仮定すると、対応するプロセスは次のようになります。

1. undo no=3 ログから id=2 のデータを削除します。

2. undo no=2 ログを使用して、id=1 のデータの削除マークを 0 に復元します。

3. undo no=1 ログを使用して、id=1 のデータの名前を Tom に復元します。

4. undo no=0 ログにより id=1 のデータを削除します。

4. ログの削除を元に戻す

  • 挿入取り消しログの場合
    • 挿入操作のレコードはトランザクション自体にのみ表示され、他のトランザクションには表示されないためです。したがって、トランザクションがコミットされた後、パージ操作を行わずに、UNDO ログを直接削除できます。
  • 更新取り消しログ用
    • Undo ログは、トランザクションがコミットされたときに削除できないように、MVCC メカニズムを提供する必要がある場合があります。送信時にそれを元に戻すログのリンク リストに追加し、パージ スレッドが最終的な削除を実行するまで待ちます。

2.2.6 概要

Undo ログは論理ログであり、トランザクションがロールバックされると、データベースが論理的に元の状態に復元されます。

REDO ログはデータ ページの物理的な変更を記録する物理ログであり、UNDO ログは REDO ログの逆のプロセスではありません。

3、ロック

トランザクションの分離は、この章で説明するロックによって実現されます。

3.1 概要

データベースでは、従来のコンピューティング リソース (CPU、RAM、I/O など) の競合に加えて、データは多くのユーザーによって共有されるリソースでもあります。データの一貫性を確保するには、同時操作を制御する必要があるため、ロックが生成されます。同時に、ロック メカニズムは、MySQL のさまざまな分離レベルの実現を保証します。ロックの競合も、データベースへの同時アクセスのパフォーマンスに影響を与える重要な要素です。したがって、ロックはデータベースにとって特に重要であり、より複雑です。

3.2 Mysql の同時トランザクションは同じレコードにアクセスします

同時トランザクションが同じレコードにアクセスする場合は、大きく 3 つのタイプに分類できます。

3.2.1 読み取り-読み取り

読み取り-読み取り状況、つまり、同時トランザクションは同じレコードを次々に読み取ります。読み取り操作自体はレコードに影響を与えず、問題も発生しないため、これが許可されます。

3.2.2 書き込み-書き込み

書き込みと書き込みの状況、つまり、同時トランザクションが同じレコードに連続して変更を加えます。

この場合、ダーティ ライトの問題が発生しますが、どの分離レベルでもこの​​問題は発生しません。したがって、コミットされていない複数のトランザクションがレコードを次々に変更する場合、トランザクションをキューに入れて実行する必要がありますが、このキューイングの処理は実際にはロックによって実現されます。このいわゆるロックは、実際にはメモリ内の構造です。トランザクションが実行される前にはロックはありません。つまり、図に示すように、最初はレコードに関連付けられたロック構造はありません。

トランザクションがこのレコードに変更を加えたい場合、まずメモリ内にこのレコードに関連付けられたロック構造があるかどうかを確認し、存在しない場合は、それに関連付けるロック構造がメモリ内に生成されます。たとえば、トランザクション T1 がこのレコードに変更を加えたい場合は、それに関連付けられたロック構造を生成する必要があります。

いくつかのステートメントを要約します。

  • ロック解除された
    • これは、メモリ内に対応するロック構造を生成する必要がなく、操作を直接実行できることを意味します。
  • ロックが正常に取得されました、またはロックが正常に取得されました
    • これは、対応するロック構造がメモリ内に生成され、ロック構造の is_waiting 属性が false、つまりトランザクションが操作の実行を継続できることを意味します。
  • ロックの取得に失敗、またはロックの取得に失敗、またはロックを取得できませんでした
    • これは、対応するロック構造がメモリ内に生成されますが、ロック構造の is_waiting 属性が true であることを意味します。つまり、トランザクションは待機する必要があり、操作の実行を続行できません。

3.2.3 読み取り/書き込み

読み取り-書き込みまたは書き込み-読み取り、つまり、1 つのトランザクションが読み取り操作を実行し、もう 1 つのトランザクションが変更操作を実行します。この場合、ダーティ リード、ノンリピータブル リード、ファントム リードが発生する可能性があります。

各データベース ベンダーは、SQL 標準のサポートが異なる場合があります。たとえば、MySQL は REPEATABLE READ 分離レベルでのファントム読み取りの問題を解決しました。

3.2.4 同時実行性の問題の解決策

ダーティ リード、反復不可能なリード、ファントム リードの問題を解決するにはどうすればよいでしょうか? 実際には、次の 2 つの解決策が考えられます。

解決策 1: 読み取り操作にはマルチバージョン同時実行制御 (MVCC、次の章で説明) を使用し、書き込み操作にはロックを使用します。

通常の SELECT ステートメントは、MVCC を使用して、READ COMMITTED および REPEATABLE READ 分離レベルでレコードを読み取ります。

  • READ COMMITTED 分離レベルでは、トランザクションは実行中に SELECT 操作を実行するたびに ReadView を生成します。ReadView 自体の存在により、トランザクションはコミットされていないトランザクションによって行われた変更を読み取ることができなくなり、ダーティ リード現象が回避されます。 ;
  • REPEATABLE READ 分離レベルでは、トランザクションの実行中に初めて SELECT 操作が実行されたときにのみ ReadView が生成され、ReadView は後続の SELECT 操作で再利用されるため、反復不可能な読み取りの問題が回避されます。そしてファントムリード。

解決策 2: 読み取り操作と書き込み操作の両方がロックされます。

  • 要約と比較が見つかりました:
    • MVCC方式を採用すると、読み書き動作が競合せず、パフォーマンスが向上します。
    • ロック方式を使用する場合は、読み取り/書き込み操作を相互にキューに入れる必要があるため、パフォーマンスに影響します。

一般に、読み取り/書き込み操作の同時実行の問題を解決するには MVCC を使用しますが、特殊なケースでは、ビジネスをロックされた方法で実行する必要があります。以下では、MySQL のさまざまなタイプのロックについて説明します。

3.3 さまざまな角度のロックの分類

ロックの分類図は次のとおりです。

3.3.1 スレーブデータ操作の種類: リードロック、ライトロック

  • 読み取りロック: 共有ロックとも呼ばれ、英語の S で表されます。同じデータに対して、複数のトランザクションの読み取り操作は、相互に影響を与えたりブロックしたりすることなく同時に実行できます。
  • 書き込みロック: 排他ロックとも呼ばれ、英語では X で表されます。現在の書き込み操作が完了する前に、他の書き込みロックと読み取りロックがブロックされます。これにより、一度に 1 つのトランザクションのみが書き込みを実行できるようになり、書き込み先の同じリソースを他のユーザーが読み取ることができなくなります。

InnoDB エンジンの場合、読み取りロックと書き込みロックをテーブルまたは行に追加できることに注意してください。

3.3.2 データ操作の粒度から: テーブルレベルのロック、ページレベルのロック、行レベルのロック

3.3.2.1 テーブルロック

①テーブルレベルのSロックとXロック

テーブルに対して SELECT、INSERT、DELETE、および UPDATE ステートメントを実行する場合、InnoDB ストレージ エンジンはテーブル レベルの S ロックまたは X ロックをテーブルに追加しません。ALTER TABLE や DROP TABLE などの一部の DDL ステートメントが特定のテーブルで実行されると、他のトランザクションによって、このテーブルに対する SELECT、INSERT、DELETE、UPDATE などのステートメントの同時実行がブロックされます。同様に、特定のトランザクションのテーブルに対して SELECT、INSERT、DELETE、および UPDATE ステートメントが実行されると、他のセッションでこのテーブルに対して実行される DDL ステートメントもブロックされます。この処理は、実際にはサーバー層でメタデータロック(英語名:Metadata Locks、略してMDL)と呼ばれる構造を利用して実現されます。

一般に、InnoDB ストレージ エンジンによって提供されるテーブル レベルの S ロックと X ロックは使用されません。クラッシュ回復時など、一部の特殊な場合にのみ使用されます。たとえば、システム変数 autocommit=0 および innodb_table_locks=1 の場合、InnoDB ストレージ エンジンによって提供されるテーブル t の S ロックまたは X ロックを手動で取得するには、次のように記述できます。

  • LOCK TABLES t READ: InnoDB ストレージ エンジンは、テーブル レベルの S ロックをテーブル t に追加します。
  • LOCK TABLES t WRITE: InnoDB ストレージ エンジンは、テーブル レベルの X ロックをテーブル t に追加します。

ただし、InnoDB ストレージ エンジンを使用するテーブルで LOCK TABLES などの手動ロック テーブル ステートメントを使用することは避けてください。追加の保護は提供されず、同時実行機能が低下するだけです。InnoDB の優れた点は、より詳細な行ロックを実装していることです。S ロックと X ロックについては InnoDB テーブル レベルで学習できます。

MySQL のテーブルレベルのロックには 2 つのモードがあります: (MyISAM テーブルでの操作のデモ)

  • テーブル共有読み取りロック (Table Read Lock)
  • テーブル排他書き込みロック(Table Write Lock)

②インテンションロック(インテンションロック)

InnoDB は、行レベルのロックとテーブル レベルのロックの共存を可能にする複数の粒度ロックをサポートしており、インテント ロックはテーブル ロックの 1 つです。

インテント ロックには 2 つのタイプがあります。

  • 意図的共有ロック (意図的共有ロック、IS): トランザクションは、テーブル内の特定の行に共有ロック (S ロック) を追加することを目的としています。
    • トランザクションが一部の行に対して S ロックを取得するには、まずテーブルに対して IS ロックを取得する必要があります。テーブルから列を選択 ... 共有モードでロック;
  • 意図的排他的ロック (意図的排他的ロック、IX): トランザクションは、テーブル内の特定の行に排他的ロック (X ロック) を追加することを目的としています。
    • トランザクションが一部の行に対して X ロックを取得するには、まずテーブルに対して IX ロックを取得する必要があります。テーブルから列を選択 ... FOR UPDATE;

つまり、インテント ロックはストレージ エンジン自体によって維持され、ユーザーはインテント ロックを手動で操作することはできません。データ行に共有/排他ロックを追加する前に、InooDB はまずデータ テーブルの対応するインテント ロックを取得します。データ行が見つかりました。

インテントロックの同時実行性

インテンション ロックは、行レベルの共有/排他ロックと相互に排他的ではありません。このため、複数のトランザクションが異なるデータ行に対して排他ロックをロックする場合、インテント ロックは同時実行性に影響しません。(それ以外の場合は、通常のテーブル ロックを使用できます)

結論は:

  1. InnoDB は複数粒度のロックをサポートしており、特定のシナリオでは、行レベルのロックがテーブル レベルのロックと共存できます。
  2. インテント ロックは相互排他的ではありませんが、IS が S と互換性があることを除き、インテント ロックは共有ロック/排他的ロックと相互排他的です。
  3. IX、IS はテーブル レベルのロックであり、行レベルの X、S ロックと競合しません。テーブルレベルの X および S とのみ競合します。
  4. インテント ロックは、同時実行性の確保を前提として、行ロックとテーブル ロックの共存を実現し、トランザクション分離の要件を満たします。

③ 自動増加ロック(AUTO-INCロック)

MySQL を使用するプロセスで、テーブルの列に AUTO_INCREMENT 属性を追加できます。例:

CREATE TABLE `teacher` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

このテーブルの id フィールドは AUTO_INCREMENT を宣言しているため、insert 文を書くときに値を代入する必要がないことを意味しており、SQL 文の修正は次のようになります。

INSERT INTO `teacher` (name) VALUES ('zhangsan'), ('lisi');

上記の挿入ステートメントは、id 列に値を明示的に割り当てないため、システムは自動的に増分値をそれに割り当てます。

上記の挿入データは単なる単純な挿入モードであり、データを挿入するすべての方法は、「単純な挿入」、「一括挿入」、および「混合モードの挿入」という 3 つのカテゴリに分類されます。

  • 「シンプルなインサート」
    • 挿入する行数を事前に決定できるステートメント (ステートメントの最初の処理時)。ネストされたサブクエリを含まない、単一行および複数行の INSERT...VALUES() および REPLACE ステートメントが含まれます。たとえば、上で示した例はこのタイプの挿入に属し、挿入される行数が決定されています。
  • 「一括挿入」
    • 挿入される行数 (および必要な自動インクリメント値の数) が事前に不明なステートメント。INSERT ... SELECT 、 REPLACE ... SELECT および LOAD DATA ステートメントなどですが、純粋な INSERT ではありません。InnoDB は、行が処理されるたびに AUTO_INCREMENT カラムに新しい値を割り当てます。
  • 「混合モードインサート」(混合モードインサート)
    • これらは「単純な挿入」ステートメントですが、いくつかの新しい行に自動インクリメント値を指定します。たとえば、 INSERT INTO Teacher (id,name)VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d'); 部分の値のみを指定しますIDの。
    • 別のタイプの「混合モード挿入」は、 INSERT ... ON DUPLICATE KEY UPDATE です。

上記のデータ挿入の場合、Mysql は自己増加ロック方式を採用しています。AUTO-INC ロックは、AUTO-INCREMENT カラムを使用するテーブルにデータを挿入するときに取得する必要がある特別なテーブル レベルのロックです。ステートメントが実行されると、テーブル レベルで AUTO-INC ロックが追加され、挿入される各レコードに増分値が割り当てられます。ステートメントの実行後、AUTO-INC ロックは解放されます。

トランザクションが AUTO-INC ロックを保持している場合、他のトランザクションの挿入ステートメントをブロックする必要があります。これにより、ステートメントに割り当てられる増分値が連続的になることが保証されます。AUTO_INCREMENT キーワードの主キーに値を挿入する場合、各ステートメントはテーブル ロックをめぐって競合する必要があります。そのような同時実行の可能性は非常に低いため、innodb は innodb_autoinc_lock_mode のさまざまな値を通じてさまざまなロック メカニズムを提供し、スケーラビリティと機能を大幅に向上させます。 SQL ステートメントのパフォーマンス。

innodb_autoinc_lock_mode には、さまざまなロック モードに対応する 3 つの値があります。

1. innodb_autoinc_lock_mode = 0 (「従来の」ロック モード)

このロック モードでは、すべての種類の挿入ステートメントが、AUTO_INCREMENT 列を持つテーブルへの挿入のために特別なテーブル レベルの AUTO-INC ロックを取得します。このモードは、実際には上記の例と似ています。つまり、挿入が実行されるたびにテーブル レベルのロック (AUTO-INC ロック) が取得されるため、ステートメント内で生成された auto_increment が適切になり、それが再生されると、 binlog では、マスターとスレーブのデータの auto_increment が同じであることが保証されます。これはテーブルレベルのロックであるため、挿入が複数のトランザクションで同時に実行されると、AUTO-INC ロックの競合により同時実行機能が制限されます。

2. innodb_autoinc_lock_mode = 1 (「継続的」ロックモード)

MySQL 8.0 より前は、順次ロック モードがデフォルトでした。このモードでは、「一括挿入」でも AUTO-INC テーブル レベルのロックが使用され、ステートメントの終わりまでロックが保持されます。これは、すべての INSERT ... SELECT 、 REPLACE ... SELECT および LOAD DATA ステートメントに適用されます。AUTO-INC ロックを保持できるステートメントは一度に 1 つだけです。「単純な挿入」(挿入される行数が事前にわかっている)の場合、ミューテックス(軽量ロック)の制御下で必要な数の自動インクリメント値を取得することで、テーブルレベルの AUTO-INC ロックが回避されます。これは、ステートメントが完了するまでではなく、ステートメントの存続期間中の割り当て中にのみ使用されます。テーブルレベルの AUTO-INC ロックは、AUTO-INC ロックが別のトランザクションによって保持されない限り使用されません。別のトランザクションが AUTO-INC ロックを保持している場合、「Simpleinserts」は「一括挿入」であるかのように AUTO-INC ロックを待ちます。

3. innodb_autoinc_lock_mode = 2 (「インターリーブ」ロック モード)

MySQL 8.0 以降、インターリーブ ロック モードがデフォルトです。このロック モードでは、自動インクリメント値が一意であることが保証され、同時に実行されるすべてのタイプの挿入ステートメントにわたって単調増加します。ただし、複数のステートメントが同時に数値を生成する (つまり、ステートメント間で相互番号付けする) 可能性があるため、特定のステートメントによって挿入された行に対して生成される値は連続しない場合があります。

④メタデータロック(MDLロック)

MySQL 5.5 では、テーブル ロックのカテゴリに属する​​、MDL ロックと呼ばれるメタ データ ロックが導入されました。MDL の役割は、読み取りと書き込みの正確性を保証することです。たとえば、クエリがテーブル内のデータを走査し、別のスレッドが実行中にテーブル構造を変更して列を追加した場合、クエリ スレッドによって取得された結果はテーブル構造と一致しないため、機能しません。

したがって、テーブルの追加、削除、変更、クエリを実行する場合は、MDL 読み取りロックを追加してください。

テーブルに対して構造変更操作を行う場合は、MDL 書き込みロックを追加します。

3.3.2.2 行ロック

① レコードロック

レコード ロックは 1 つのレコードのみをロックするもので、正式な型名は LOCK_REC_NOT_GAP です。たとえば、id 値が 8 のレコードにレコード ロックを追加する概略図を図に示します。ID 値が 8 のレコードのみをロックし、周囲のデータには影響しません。

例:

セッション1

セッション2

mysql> 自動コミット =0 を設定します。

mysql> 自動コミット =0 を設定します。

mysql> update Student set name = '张三' where id = 1;

mysql> 学生セット名 = '李四1' を更新します (id = 3;)。

クエリ OK、1 行が影響を受ける (0.00 秒)

一致した行: 1 変更された: 1 警告: 0

mysql> 学生セット名 = '张三1' を更新、ここで ID = 1;

エラー 1205 (HY000): ロック待機タイムアウトを超えました。トランザクションを再開してみてください

mysql> コミット;

クエリは OK、影響を受ける行は 0 (0.00 秒)

mysql> 学生セット名 = '张三1' を更新、ここで ID = 1;

クエリ OK、1 行が影響を受ける (0.00 秒)

一致した行: 1 変更された: 1 警告: 0

mysql> コミット;

クエリは OK、影響を受ける行は 0 (0.00 秒)

レコードロックはSロックとXロックに分かれており、それぞれS型レコードロック、X型レコードロックと呼ばれます。

  • トランザクションがレコードの S タイプ レコード ロックを取得すると、他のトランザクションはそのレコードの S タイプ レコード ロックを引き続き取得できますが、X タイプ レコード ロックを取得し続けることはできません。
  • トランザクションがレコードの X タイプ レコード ロックを取得すると、他のトランザクションはレコードの S タイプ レコード ロックも X タイプ レコード ロックも取得し続けることはできません。

②ギャップロック

MySQL では、REPEATABLE READ 分離レベルでのファントム読み取りの問題を解決できます。MVCC ソリューションを使用するか、ロック ソリューションを使用することで解決できる 2 つの解決策があります。

しかし、ロック ソリューションを使用する場合には大きな問題があります。つまり、トランザクションが初めて読み取り操作を実行するとき、それらのファントム レコードはまだ存在せず、これらのファントム レコードにレコード ロックを追加することはできません。

InnoDB は、Gap Locks と呼ばれるロックを提案しています。正式な型名は LOCK_GAP で、単にギャップ ロックと呼ぶことができます。たとえば、id 値が 8 のレコードにギャップ ロックを追加する概略図は次のとおりです。

图中id值为8的记录加了gap锁,意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录 ,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。

gap锁的提出仅仅是为了防止插入幻影记录而提出的。

③ 临键锁(Next-Key Locks)

有时候我们既想 锁住某条记录 ,又想阻止其他事务在该记录前边的间隙插入新记录 ,所以InnoDB就提出了一种称之为 Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,我们也可以简称为next-key锁 。Next-Key Locks是在存储引擎 innodb 、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。

begin; 
select * from student where id <=8 and id > 3 for update;

④ 插入意向锁(Insert Intention Locks)

我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁 ( next-key锁也包含 gap锁 ),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙 中 插入 新记录,但是现在在等待。InnoDB就把这种类型的锁命名为 Insert Intention Locks ,官方的类型名称为:LOCK_INSERT_INTENTION ,我们称为插入意向锁 。

插入意向锁是一种 Gap锁 ,不是意向锁,在insert操作时产生。插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁 。

3.3.2.3 页锁

页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

每个层级的锁数量是有限制的,因为锁会占用内存空间, 锁空间的大小是有限的 。当某个层级的锁数量超过了这个层级的阈值时,就会进行 锁升级 。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

3.3.3 从对待锁的态度上:乐观锁、悲观锁

从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式 。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想 。

3.3.3.1 悲观锁

悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞 直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

3.3.3.2 乐观锁

乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者CAS机制实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中 java.util.concurrent.atomic 包下的原子变量类就是使用了乐观锁。

1. 乐观锁的版本号机制

在表中设计一个 版本字段 version ,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行 UPDATE ... SET version=version+1 WHERE version=version 。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

2. 乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。

能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。

3.3.3.2 两种锁的适用场景

从这两种锁的设计思想中,我们总结一下乐观锁和悲观锁的适用场景:

  1. 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现 , 不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  2. 悲观锁适合写操作多的场景,因为写的操作具有排它性 。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。

3.3.4 按加锁的方式划分:显示锁、隐式锁

3.3.4.1 隐式锁

  • 情景一:对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的 事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构, is_waiting 属性是 true )。
  • 情景二:对于二级索引记录来说,本身并没有 trx_id 隐藏列,但是在二级索引页面的 PageHeader 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复情景一的做法。

session 1:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert INTO student VALUES(34,"周八","二班");
Query OK, 1 row affected (0.00 sec)

session 2:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student lock in share mode; #执行完,当前事务被阻塞

执行下述语句,输出结果:

mysql> SELECT * FROM performance_schema.data_lock_waits\G;
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 4571466880:4212:4:9:4999713136
REQUESTING_ENGINE_TRANSACTION_ID: 281479548177536
            REQUESTING_THREAD_ID: 48
             REQUESTING_EVENT_ID: 80
REQUESTING_OBJECT_INSTANCE_BEGIN: 4999713136
         BLOCKING_ENGINE_LOCK_ID: 4571467672:4212:4:9:4999717400
  BLOCKING_ENGINE_TRANSACTION_ID: 99918
              BLOCKING_THREAD_ID: 48
               BLOCKING_EVENT_ID: 80
  BLOCKING_OBJECT_INSTANCE_BEGIN: 4999717400
1 row in set (0.00 sec)

隐式锁的逻辑过程如下:

A. InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+Tree中。

B. 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务,首先将隐式锁转换为显式锁 (就是为该事务添加一个锁)。

C. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到E。

D. 等待加锁成功,被唤醒,或者超时。

E. 写数据,并将自己的trx_id写入trx_id字段。

3.3.4.2 显式锁

通过特定的语句进行加锁,我们一般称之为显示加锁,例如:

显示加共享锁:

select .... lock in share mode

显示加排它锁:

select .... for update

3.3.5 其他锁:全局锁

全局锁就是对 整个数据库实例 加锁。当你需要让整个库处于 只读状态 的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。全局锁的典型使用 场景 是:做 全库逻辑备份 。

全局锁的命令:

Flush tables with read lock

3.3.6 其他锁:死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。死锁示例:

事务1

事务2

start transaction;

update student set money=10 where id=1;

start transaction;

update account set money=10 where id=2;

update account set money=20 where id=2;

update account set money=20 where id=1;

这时候,事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。 事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。当出现死锁以后,有 两种策略 :

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on ,表示开启这个逻辑

第二种策略的成本分析

方法1:如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关掉。但是这种操作本身带有一定的风险,因为业务设计的时候一般不会把死锁当做一个严重错误,毕竟出现死锁了,就回滚,然后通过业务重试一般就没问题了,这是 业务无损 的。而关掉死锁检测意味着可能会出现大量的超时,这是业务有损的

方法2:控制并发度。如果并发能够控制住,比如同一行同时最多只有10个线程在更新,那么死锁检测的成本很低,就不会出现这个问题。这个并发控制要做在 数据库服务端 。如果你有中间件,可以考虑在 中间件实现 ;甚至有能力修改MySQL源码的人,也可以做在MySQL里面。基本思路就是,对于相同行的更新,在进入引擎之前排队,这样在InnoDB内部就不会有大量的死锁检测工作了。

3.4 锁的内存结构 

InnoDB 存储引擎中的 锁结构 如下:

结构解析:

1. 锁所在的事务信息 :

不论是 表锁 还是 行锁 ,都是在事务执行过程中生成的,哪个事务生成了这个 锁结构 ,这里就记录这个事务的信息。

此 锁所在的事务信息 在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息,比方说事务id等。

2. 索引信息 :

对于 行锁 来说,需要记录一下加锁的记录是属于哪个索引的。这里也是一个指针。

3. 表锁/行锁信息 :

表锁结构 和 行锁结构 在这个位置的内容是不同的:

  • 表锁:
    • 记载着是对哪个表加的锁,还有其他的一些信息。
  • 行锁:记载了三个重要的信息:
    • Space ID :记录所在表空间。
    • Page Number :记录所在页号。
    • n_bits :对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。为此在行锁结构的末尾放置了一堆比特位,这个n_bits 属性代表使用了多少比特位。
      • n_bits的值一般都比页面中记录条数多一些。主要是为了之后在页面中插入了新记录后也不至于重新分配锁结构

4. type_mode

这是一个32位的数,被分成了 lock_mode 、 lock_type 和 rec_lock_type 三个部分,如图所示

  • 锁的模式( lock_mode ),占用低4位,可选的值如下:
    • LOCK_IS (十进制的 0 ):表示共享意向锁,也就是 IS锁 。
    • LOCK_IX (十进制的 1 ):表示独占意向锁,也就是 IX锁 。
    • LOCK_S (十进制的 2 ):表示共享锁,也就是 S锁 。
    • LOCK_X (十进制的 3 ):表示独占锁,也就是 X锁 。
    • LOCK_AUTO_INC (十进制的 4 ):表示 AUTO-INC锁 。

在InnoDB存储引擎中,LOCK_IS,LOCK_IX,LOCK_AUTO_INC都算是表级锁的模式,LOCK_S和LOCK_X既可以算是表级锁的模式,也可以是行级锁的模式。

  • 锁的类型( lock_type ),占用第5~8位,不过现阶段只有第5位和第6位被使用:
    • LOCK_TABLE ,当第5个比特位置为1时,表示表级锁。
    • LOCK_REC ),第6个比特位置为1时,表示行级锁。
  • 行锁的具体类型( rec_lock_type ),使用其余的位来表示。只有在 lock_type 的值为LOCK_REC 时,也就是只有在该锁为行级锁时,才会被细分为更多的类型:
    • LOCK_ORDINARY:表示 next-key锁 。
    • LOCK_GAP:当第10个比特位置为1时,表示 gap锁 。
    • LOCK_REC_NOT_GAP:当第11个比特位置为1时,表示正经 记录锁 。
    • LOCK_INSERT_INTENTION :当第12个比特位置为1时,表示插入意向锁。
    • 其他的类型:还有一些不常用的类型我们就不多说了。
  • is_waiting 属性呢?基于内存空间的节省,所以把 is_waiting 属性放到了 type_mode 这个32位的数字中:
    • LOCK_WAIT (十进制的 256 ) :当第9个比特位置为 1 时,表示 is_waiting 为 true ,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为false ,也就是当前事务获取锁成功。

5. 其他信息

为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。

6. 一堆比特位

如果是行锁结构的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits属性表示的。InnoDB数据页中的每条记录在记录头信息中都包含一个 heap_no 属性,伪记录Infimum 的heap_no值为0 , Supremum 的heap_no值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结构 最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no ,即一个比特位映射到页内的一条记录。

3.5 锁监控

3.5.1 InnoDB_row_lock

关于MySQL锁的监控,我们一般可以通过检查 InnoDB_row_lock 等状态变量来分析系统上的行锁的争夺情况。

mysql> show status like 'innodb_row_lock%';
+-------------------------------+--------+
| Variable_name                 | Value  |
+-------------------------------+--------+
| Innodb_row_lock_current_waits | 0      |
| Innodb_row_lock_time          | 145647 |
| Innodb_row_lock_time_avg      | 29129  |
| Innodb_row_lock_time_max      | 50188  |
| Innodb_row_lock_waits         | 5      |
+-------------------------------+--------+
5 rows in set (0.00 sec)

对各个状态量的说明如下:

  • Innodb_row_lock_current_waits:当前正在等待锁定的数量;
  • Innodb_row_lock_time :从系统启动到现在锁定总时间长度;(等待总时长)
  • Innodb_row_lock_time_avg :每次等待所花平均时间;(等待平均时长)
  • Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
  • Innodb_row_lock_waits :系统启动后到现在总共等待的次数;(等待总次数)

对于这5个状态变量,比较重要的3个见上面(橙色)。

3.5.2 其他监控方法

MySQL把事务和锁的信息记录在了 information_schema 库中,涉及到的三张表分别是INNODB_TRX 、 INNODB_LOCKS 和 INNODB_LOCK_WAITS 。

MySQL5.7及之前 ,可以通过information_schema.INNODB_LOCKS查看事务的锁情况,但只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况。

MySQL8.0删除了information_schema.INNODB_LOCKS,添加了 performance_schema.data_locks ,可以通过performance_schema.data_locks查看事务的锁情况,和MySQL5.7及之前不同,performance_schema.data_locks不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁。

同时,information_schema.INNODB_LOCK_WAITS也被 performance_schema.data_lock_waits 所代替。

我们模拟一个锁等待的场景,以下是从这三张表收集的信息锁等待场景,我们依然使用记录锁中的案例,当事务2进行等待时,查询情况如下:

Session1

Session2

mysql> begin;

mysql> begin;

mysql> update student set name = '张三' where id = 1;

mysql> update student set name = '李四1' where id = 3;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

mysql> update student set name = '张三1' where id = 1;

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

mysql> update student set name = '张三1' where id = 1;

Query OK, 1 row affected (0.00 sec)

Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

1、查询正在被锁阻塞的sql语句

SELECT * FROM information_schema.INNODB_TRX\G;

2、查询锁等待情况

mysql> use performance_schema;
Database changed
mysql> SELECT * FROM data_lock_waits\G;
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 4571467672:4212:4:8:4999718088
REQUESTING_ENGINE_TRANSACTION_ID: 99927
            REQUESTING_THREAD_ID: 49
             REQUESTING_EVENT_ID: 78
REQUESTING_OBJECT_INSTANCE_BEGIN: 4999718088
         BLOCKING_ENGINE_LOCK_ID: 4571466880:4212:4:8:4999712792
  BLOCKING_ENGINE_TRANSACTION_ID: 99926
              BLOCKING_THREAD_ID: 48
               BLOCKING_EVENT_ID: 95
  BLOCKING_OBJECT_INSTANCE_BEGIN: 4999712792
1 row in set (0.00 sec)

3、查询锁的情况

mysql> SELECT * from performance_schema.data_locks\G;
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571467672:5273:4983917304
ENGINE_TRANSACTION_ID: 99927
            THREAD_ID: 49
             EVENT_ID: 76
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 4983917304
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571467672:4212:4:15:4999717400
ENGINE_TRANSACTION_ID: 99927
            THREAD_ID: 49
             EVENT_ID: 76
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 4999717400
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 3
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571466880:5273:4983916280
ENGINE_TRANSACTION_ID: 99926
            THREAD_ID: 48
             EVENT_ID: 95
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 4983916280
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 4. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 4571466880:4212:4:8:4999712792
ENGINE_TRANSACTION_ID: 99926
            THREAD_ID: 48
             EVENT_ID: 95
        OBJECT_SCHEMA: atguigudb
          OBJECT_NAME: student
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 4999712792
            LOCK_TYPE: RECORD
            LOCK_MODE: X,REC_NOT_GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 1
4 rows in set (0.00 sec)

从锁的情况可以看出来,两个事务分别获取了IX锁,我们从意向锁章节可以知道,IX锁互相时兼容的。所以这里不会等待,但是事务1同样持有X锁,此时事务2也要去同一行记录获取X锁,他们之间不兼容,导致等待的情况发生。

3.6 附录

间隙锁加锁规则(共11个案例)

间隙锁是在可重复读隔离级别下才会生效的: next-key lock 实际上是由间隙锁加行锁实现的,如果切换到读提交隔离级别 (read-committed) 的话,就好理解了,过程中去掉间隙锁的部分,也就是只剩下行锁的部分。而在读提交隔离级别下间隙锁就没有了,为了解决可能出现的数据和日志不一致问题,需要把binlog 格式设置为 row 。也就是说,许多公司的配置为:读提交隔离级别加 binlog_format=row。业务不需要可重复读的保证,这样考虑到读提交下操作数据的锁范围更小(没有间隙锁),这个选择是合理的。

next-key lock的加锁规则

总结的加锁规则里面,包含了两个 “ “ 原则 ” ” 、两个 “ “ 优化 ” ” 和一个 “bug” 。

  • 原则 1 :加锁的基本单位是 next-key lock 。 next-key lock 是前开后闭区间。
  • 原则 2 :查找过程中访问到的对象才会加锁。任何辅助索引上的锁,或者非索引列上的锁,最终都要回溯到主键上,在主键上也要加一把锁。
  • 优化 1 :索引上的等值查询,给唯一索引加锁的时候, next-key lock 退化为行锁。也就是说如果InnoDB扫描的是一个主键、或是一个唯一索引的话,那InnoDB只会采用行锁方式来加锁
  • 优化 2 :索引上(不一定是唯一索引)的等值查询,向右遍历时且最后一个值不满足等值条件的时候, next-keylock 退化为间隙锁。
  • 一个 bug :唯一索引上的范围查询会访问到不满足条件的第一个值为止。

我们以表test作为例子,建表语句和初始化语句如下:其中id为主键索引

CREATE TABLE `test` (
`id` int(11) NOT NULL,
`col1` int(11) DEFAULT NULL,
`col2` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`col1`)
) ENGINE=InnoDB;
insert into test values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

案例一:唯一索引等值查询间隙锁

sessionA

sessionB

sessionC

update test set col2 = col2+1 where id=7;

insert into test values(8,8,8);

(blocked)

update test set col2 = col2+1 where id=10;

(Query OK)

由于表 test 中没有 id=7 的记录.

根据原则 1 ,加锁单位是 next-key lock , session A 加锁范围就是 (5,10] ; 同时根据优化 2 ,这是一个等值查询 (id=7) ,而 id=10 不满足查询条件, next-key lock 退化成间隙锁,因此最终加锁的范围是 (5,10)

案例二:非唯一索引等值查询锁

sessionA

sessionB

sessionC

select id from test where col1 = 5 lock in share mode;

update test set col2 = col2+1 where id=5;(Query OK)

insert into test values(7,7,7)

(blocked)

这里 session A 要给索引 col1 上 col1=5 的这一行加上读锁。

  1. 根据原则 1 ,加锁单位是 next-key lock ,左开右闭,5是闭上的,因此会给 (0,5] 加上 next-key lock。
  2. 要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的(可能有col1=5的其他记录),需要向右遍历,查到c=10 才放弃。根据原则 2 ,访问到的都要加锁,因此要给 (5,10] 加next-key lock 。
  3. 但是同时这个符合优化 2 :等值判断,向右遍历,最后一个值不满足 col1=5 这个等值条件,因此退化成间隙锁 (5,10) 。
  4. 根据原则 2 , 只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。

但 session C 要插入一个 (7,7,7) 的记录,就会被 session A 的间隙锁 (5,10) 锁住 这个例子说明,锁是加在索引上的。执行 for update 时,系统会认为你接下来要更新数据,因此会顺便给主键索引上满足条件的行加上行锁。如果你要用 lock in share mode来给行加读锁避免数据被更新的话,就必须得绕过覆盖索引的优化,因为覆盖索引不会访问主键索引,不会给主键索引上加锁.

案例三:主键索引范围查询锁

上面两个例子是等值查询的,这个例子是关于范围查询的,也就是说下面的语句

select * from test where id=10 for updates elect * from tets where id>=10 and id<11 for update;

这两条查语句肯定是等价的,但是它们的加锁规则不太一样

sessionA

sessionB

sessionC

select * from test where id>= 10 and id

insert into testvalues(8,8,8);

(Query OK)

insert into testvalues(13,13,13);(blocked)

update test set clo2=col2+1where id=15;(blocked)

  1. 开始执行的时候,要找到第一个 id=10 的行,因此本该是 next-key lock(5,10] 。 根据优化 1 ,主键id 上的等值条件,退化成行锁,只加了 id=10 这一行的行锁。
  2. 它是范围查询, 范围查找就往后继续找,找到 id=15 这一行停下来,不满足条件,因此需要加next-key lock(10,15] 。

session A 这时候锁的范围就是主键索引上,行锁 id=10 和 next-key lock(10,15] 。首次 session A 定位查找id=10 的行的时候,是当做等值查询来判断的,而向右扫描到 id=15 的时候,用的是范围查询判断。

案例四:非唯一索引范围查询锁

select * from test where col1=10 for updates elect * from tets where col1>=10 and col1<11 for update;

与案例三不同的是,案例四中查询语句的 where 部分用的是字段 c ,它是普通索引这两条查语句肯定是等价的,但是它们的加锁规则不太一样

sessionA

sessionB

sessionC

select * from test where col1>= 10 and col1

insert into testvalues(8,8,8);

((blocked)

update test set clo2=col2+1 where id=15;(blocked)

在第一次用 col1=10 定位记录的时候,索引 c 上加了 (5,10] 这个 next-key lock 后,由于索引 col1 是非唯一索引,没有优化规则,也就是 说不会蜕变为行锁,因此最终 sesion A 加的锁是,索引 c 上的 (5,10] 和(10,15] 这两个 next-keylock 。

这里需要扫描到 col1=15 才停止扫描,是合理的,因为 InnoDB 要扫到 col1=15 ,才知道不需要继续往后找了。

案例五:唯一索引范围查询锁 bug

sessionA

sessionB

sessionC

select * from test where id> 10 andid

update test set clo2=col2+1where id=20;(blocked)

insert into testvalues(16,16,16);(blocked)

session A 是一个范围查询,按照原则 1 的话,应该是索引 id 上只加 (10,15] 这个 next-key lock ,并且因为 id 是唯一键,所以循环判断到 id=15 这一行就应该停止了。

但是实现上, InnoDB 会往前扫描到第一个不满足条件的行为止,也就是 id=20 。而且由于这是个范围扫描,因此索引 id 上的 (15,20] 这个 next-key lock 也会被锁上。照理说,这里锁住 id=20 这一行的行为,其实是没有必要的。因为扫描到 id=15 ,就可以确定不用往后再找了。

案例六:非唯一索引上存在 " " 等值 " " 的例子

这里,我给表 t 插入一条新记录:insert into t values(30,10,30);也就是说,现在表里面有两个c=10的行但是它们的主键值 id 是不同的(分别是 10 和 30 ),因此这两个c=10 的记录之间,也是有间隙的。

sessionA

sessionB

sessionC

delete from test where col1=10;

insert into test values(12,12,12);(blocked)

update test set col2=col2+1 where col1=15;(blocked)

这次我们用 delete 语句来验证。注意, delete 语句加锁的逻辑,其实跟 select ... for update 是类似的,也就是我在文章开始总结的两个 “ 原则 ” 、两个 “ 优化 ” 和一个 “bug” 。

这时, session A 在遍历的时候,先访问第一个 col1=10 的记录。同样地,根据原则 1 ,这里加的是(col1=5,id=5) 到 (col1=10,id=10) 这个 next-key lock 。

由于c是普通索引,所以继续向右查找,直到碰到 (col1=15,id=15) 这一行循环才结束。根据优化 2 ,这是一个等值查询,向右查找到了不满足条件的行,所以会退化成 (col1=10,id=10) 到 (col1=15,id=15) 的间隙锁.

 这个 delete 语句在索引 c 上的加锁范围,就是上面图中蓝色区域覆盖的部分。这个蓝色区域左右两边都是虚线,表示开区间,即 (col1=5,id=5) 和 (col1=15,id=15) 这两行上都没有锁.

案例七: limit 语句加锁

例子 6 也有一个对照案例,场景如下所示:

sessionA

sessionB

delete from test where col1=10 limit 2;

insert into test values(12,12,12);(Query OK)

session A 的 delete 语句加了 limit 2 。你知道表 t 里 c=10 的记录其实只有两条,因此加不加 limit 2 ,删除的效果都是一样的。但是加锁效果却不一样

这是因为,案例七里的 delete 语句明确加了 limit 2 的限制,因此在遍历到 (col1=10, id=30) 这一行之后,满足条件的语句已经有两条,循环就结束了。因此,索引 col1 上的加锁范围就变成了从( col1=5,id=5)到( col1=10,id=30) 这个前开后闭区间,如下图所示:

 这个例子对我们实践的指导意义就是, 在删除数据的时候尽量加 limit 。

这样不仅可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。

案例八:一个死锁的例子

sessionA

sessionB

select id from test where col1=10 lockin share mode;

update test set col2=col2+1 where c=10;

(blocked)

insert into test values(8,8,8);

ERROR 1213(40001):Deadlock found when trying togetlock;try restarting transaction

  1. session A 启动事务后执行查询语句加 lock in share mode ,在索引 col1 上加了 next-keylock(5,10] 和间隙锁 (10,15) (索引向右遍历退化为间隙锁);
  2. session B 的 update 语句也要在索引 c 上加 next-key lock(5,10] ,进入锁等待; 实际上分成了两步,先是加 (5,10) 的间隙锁,加锁成功;然后加 col1=10 的行锁,因为sessionA上已经给这行加上了读锁,此时申请死锁时会被阻塞
  3. 然后 session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁, InnoDB 让session B 回滚

案例九:order by索引排序的间隙锁1

如下面一条语句

select * from test where id>9 and id<12 order by id desc for update;

下图为这个表的索引id的示意图。

  1. 首先这个查询语句的语义是 order by id desc ,要拿到满足条件的所有行,优化器必须先找到 “ 第一个 id
  2. 这个过程是通过索引树的搜索过程得到的,在引擎内部,其实是要找到 id=12 的这个值,只是最终没找到,但找到了 (10,15) 这个间隙。( id=15 不满足条件,所以 next-key lock 退化为了间隙锁 (10,15) 。)
  3. 然后向左遍历,在遍历过程中,就不是等值查询了,会扫描到 id=5 这一行,又因为区间是左开右闭的,所以会加一个next-key lock (0,5] 。 也就是说,在执行过程中,通过树搜索的方式定位记录的时候,用的是 “ 等值查询 ” 的方法。

案例十:order by索引排序的间隙锁2

sessionA

sessionB

select * from test where col1>=15 and c

insert into testvalues(6,6,6);

(blocked)

  1. 由于是 order by col1 desc ,第一个要定位的是索引 col1 上 “ 最右边的 ”col1=20 的行。这是一个非唯一索引的等值查询:左开右闭区间,首先加上 next-key lock (15,20] 。 向右遍历,col1=25不满足条件,退化为间隙锁 所以会加上间隙锁(20,25) 和 next-key lock (15,20] 。
  2. 在索引 col1 上向左遍历,要扫描到 col1=10 才停下来。同时又因为左开右闭区间,所以 next-keylock 会加到 (5,10] ,这正是阻塞session B 的 insert 语句的原因。
  3. 在扫描过程中, col1=20 、 col1=15 、 col1=10 这三行都存在值,由于是 select * ,所以会在主键id 上加三个行锁。 因此, session A 的 select 语句锁的范围就是:1. 索引 col1 上 (5, 25) ;2. 主键索引上 id=15 、 20 两个行锁。

案例十一:update修改数据的例子-先插入后删除

sessionA

sessionB

select col1 from test where col1>5 lock in share mode;

update test set col1=1 where col1=5

(Query OK)

update test set col1=5 where col1=1;

(blocked)

注意:根据 col1>5 查到的第一个记录是 col1=10 ,因此不会加 (0,5] 这个 next-key lock 。

session A 的加锁范围是索引 col1 上的 (5,10] 、 (10,15] 、 (15,20] 、 (20,25] 和(25,supremum] 。

之后 session B 的第一个 update 语句,要把 col1=5 改成 col1=1 ,可以理解为两步:

  1. 插入 (col1=1, id=5) 这个记录;
  2. 删除 (col1=5, id=5) 这个记录。

通过这个操作, session A 的加锁范围变成了图示的样子:

 接下来 session B 要执行 update t set col1 = 5 where col1 = 1 这个语句了,一样地可以拆成两步:

  1. 插入 (col1=5, id=5) 这个记录;
  2. 删除 (col1=1, id=5) 这个记录。 第一步试图在已经加了间隙锁的 (1,10) 中插入数据,所以就被堵住了。

四、多版本并发控制

4.1 什么是MVCC

MVCC (Multiversion Concurrency Control),多版本并发控制。顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的并发控制 。这项技术使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。

MVCC没有正式的标准,在不同DBMS中MVCC的实现方式可能是不同的,这里讲的是InnoDB的MVCC实现机制(Mysql的其他存储引擎不支持MVCC)。

4.2 快照读与当前读

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突 ,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的就是快照读 , 而非当前读 。

当前读实际上是一种加锁的操作,是悲观锁的实现。

而MVCC本质是采用乐观锁思想的一种方式。

4.2.1 快照读

快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读;比如这样:

SELECT * FROM player WHERE ...

之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于MVCC,它在很多情况下,避免了加锁操作,降低了开销。

既然是基于多版本,那么快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

4.2.2 当前读

当前读读取的是记录的最新版本(最新数据,而不是历史版本的数据),读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的 SELECT,或者对数据进行增删改都会进行当前读。比如:

SELECT * FROM student LOCK IN SHARE MODE; # 共享锁
SELECT * FROM student FOR UPDATE; # 排他锁
INSERT INTO student values ... # 排他锁
DELETE FROM student WHERE ... # 排他锁
UPDATE student SET ... # 排他锁

4.3 复习

4.3.1 再谈隔离级别

我们知道事务有 4 个隔离级别,可能存在三种并发问题:

 另图:

 4.3.2 隐藏字段、Undo log 版本链

回顾一下undo日志的版本链,对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列。

  • trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id 隐藏列。
  • roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

假设插入记录的事务id是8.那么该条记录示意图如下:

 insert undo只在事务回滚时起作用,当事务提交后,该类型的undo日志就没用了,它占用的UndoLog Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用,要么被释放)。

假设之后两个事务id分别为 10 、 20 的事务对这条记录进行 UPDATE 操作,操作流程如下:

顺序

事务10

事务20

1

BEGIN;

2

BEGIN;

3

UPDATE student SET name="李四"WHERE id=1;

4

UPDATE student SET name="王五"WHERE id=1;

5

COMMIT;

6

UPDATE student SET name="钱七"WHERE id=1;

7

UPDATE student SET name="宋八"WHERE id=1;

8

COMMIT;

每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性( INSERT 操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表:

 对该记录每次更新后,都会将旧值放到一条undo志 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链 ,版本链的头节点就是当前记录最新的值。

每个版本中还包含生成该版本时对应的事务id 。

4.4 ReadView

MVCC 的实现依赖于:隐藏字段、Undo Log、Read View。

4.4.1 概念

在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo log中。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?这时候就需要用到ReadView了,它帮我们解决了行的可见性问题。

ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会产生数据库系统当前的一个快照,InnoDB为每一个事务创建了一个数组,用来记录并维护当前活跃事务的ID(活跃指的是,启动了但还没提交)。

4.4.2 设计思路

使用READ UNCOMMITTED隔离级别的事务,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。

使用SERIALIZABLE隔离级别的事务,InnoDB规定使用加锁的方式来访问记录。

使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务,都必须保证读到已经提交了的 事务修改过的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。

这个ReadView中主要包含4个比较重要的内容,分别如下:

  1. creator_trx_id ,创建这个 Read View 的事务 ID。
  2. trx_ids ,表示在生成ReadView时当前系统中活跃的读写事务的 事务id列表 。
  3. up_limit_id ,活跃的事务中最小的事务 ID。
  4. low_limit_id ,表示生成ReadView时系统中应该分配给下一个事务的 id 值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。

注意:low_limit_id并不是trx_ids中的最大值,事务id是递增分配的。比如,现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么一个新的读事务在生成ReadView时,trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是4。

4.4.3 ReadView规则

有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见。

  • 如果被访问版本的trx_id属性值与ReadView中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的 up_limit_id 值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的 low_limit_id 值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadView的 up_limit_id 和 low_limit_id 之间,那就需要判断一下trx_id属性值是不是在 trx_ids 列表中。
    • 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
    • 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

4.4.4 MVCC整体操作流程

了解了这些概念之后,我们来看下当查询一条记录的时候,系统如何通过MVCC找到它:

  1. 首先获取事务自己的版本号,也就是事务 ID;
  2. 获取 ReadView;
  3. 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
  4. 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
  5. 最后返回符合规则的数据。

在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View。如表所示:

 注意,此时同样的查询语句都会重新获取一次 Read View,这时如果 Read View 不同,就可能产生不可重复读或者幻读的情况。

当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会获取一次 Read View,而后面所有的 SELECT 都会复用这个 Read View,如下表所示:

4.5 举例说明

4.5.1 READ COMMITTED隔离级别

READ COMMITTED :每次读取数据前都生成一个ReadView。

现在有两个 事务id 分别为 10 、 20 的事务在执行:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...

此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:

 假设现在有一个使用 READ COMMITTED 隔离级别的事务开始执行:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'

之后,我们把 事务id 为 10 的事务提交一下:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;

然后再到 事务id 为 20 的事务中更新一下表 student 中 id 为 1 的记录:

# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student中 id 为 1 的记录的版本链就长这样:

 然后再到刚才使用 READ COMMITTED 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:

# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'王五'

4.5.2 REPEATABLE READ隔离级别

使用 REPEATABLE READ 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了。

比如,系统里有两个事务id分别为 10 、 20 的事务在执行:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...

此刻,表student 中 id 为 1 的记录得到的版本链表如下所示:

 假设现在有一个使用 REPEATABLE READ 隔离级别的事务开始执行:

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'

之后,我们把 事务id为10的事务提交一下:

# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;

然后再到事务id为20的事务中更新一下表student中id为1的记录:

# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;

此刻,表student中 id 为 1 的记录的版本链就长这样:

 然后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个 id 为 1 的记录,如下:

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值仍为'张三'

4.5.3 如何解决幻读

接下来说明InnoDB是如何解决幻读的。

假设现在表 student 中只有一条数据,数据内容中,主键 id=1,隐藏的 trx_id=10,它的undo log 如下图所示。

 假设现在有事务A和事务B并发执行, 事务A的事务id为20,事务B的事务id为30。

步骤1:事务A开始第一次查询数据,查询的SQL语句如下。

select * from student where id >= 1;

在开始查询之前,MySQL会为事务A产生一个 ReadView,此时 ReadView的内容如下:

trx_ids=[20,30] , up_limit_id=20 , low_limit_id=31 , creator_trx_id=20 。

由于此时表student中只有一条数据,且符合where id>=1条件,因此会查询出来。然后根据 ReadView机制,发现该行数据的trx_id=10,小于事务A的ReadView里up_limit_id,这表示这条数据是事务A开启之前,其他事务就已经提交了的数据,因此事务A可以读取到。

结论:事务A的第一次查询,能读取到一条数据,id=1。

步骤2:接着事务 B(trx_id=30),往表 student 中新插入两条数据,并提交事务。

insert into student(id,name) values(2,'李四');
insert into student(id,name) values(3,'王五');

此时表student中就有三条数据了,对应的undo 如下图所示:

 步骤3:接着事务A开启第二次查询,根据可重复读隔离级别的规则,此时事务A并不会再重新生成ReadView。此时表student中的3 条数据都满足where id>=1的条件,因此会先查出来。然后根据ReadView机制,判断每条数据是不是都可以被事务A看到。

1)首先id=1的这条数据,前面已经说过了,可以被事务A看到。

2)然后是id=2的数据,它的trx_id=30,此时事务A发现,这个值处于up_limit_id 和low_limit_id 之间,因此还需要再判断30是否处于trx_ids 数组内。由于事务A的trx_ids=[20,30],因此在数组内,这表示id=2的这条数据是与事务A在同一时刻启动的其他事务提交的,所以这条数据不能让事务A看到。

3)同理,id=3 的这条数据,trx_id也为30,因此也不能被事务A看见。

结论:最终事务A的第二次查询,只能查询出id=1的这条数据。这和事务A的第一次查询的结果是一样的,因此没有出现幻读现象,所以说在MySQL的可重复读隔离级别下,不存在幻读问题。

4.6 总结

这里介绍了MVCC在 READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的读-写 、 写-读 操作并发执行,从而提升系统性能。

核心点在于ReadView的原理, READ COMMITTD、REPEATABLE READ 这两个隔离级别的一个很大不同就是生成ReadView的时机不同:

  • READ COMMITTD 在每一次进行普通SELECT操作前都会生成一个ReadView
  • REPEATABLE READ 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

五、其他数据库日志

千万不要小看日志。很多看似奇怪的问题,答案往往就藏在日志里。很多情况下,只有通过查看日志才能发现问题的原因,真正解决问题。所以,一定要学会查看日志,养成检查日志的习惯,对提升你的数据库应用开发能力至关重要。

MySQL8.0 官网日志地址:“ MySQL :: MySQL 8.0 Reference Manual :: 5.4 MySQL Server Logs

5.1 Mysql支持的日志

5.1.1 日志类型

MySQL有不同类型的日志文件,用来存储不同类型的日志,分为二进制日志、错误日志、 通用查询日志和慢查询日志,这也是常用的4种。MySQL 8又新增两种支持的日志:中继日志和数据定义语句日志 。使用这些日志文件,可以查看MySQL内部发生的事情。

这6类日志分别为:

  • 慢查询日志:记录所有执行时间超过long_query_time的所有查询,方便我们对查询进行优化。
  • 通用查询日志:记录所有连接的起始时间和终止时间,以及连接发送给数据库服务器的所有指令,对我们复原操作的实际场景、发现问题,甚至是对数据库操作的审计都有很大的帮助。
  • 错误日志:记录MySQL服务的启动、运行或停止MySQL服务时出现的问题,方便我们了解服务器的状态,从而对服务器进行维护。
  • 二进制日志:记录所有更改数据的语句,可以用于主从服务器之间的数据同步,以及服务器遇到故障时数据的无损失恢复。
  • 中继日志:用于主从服务器架构中,从服务器用来存放主服务器二进制日志内容的一个中间文件。从服务器通过读取中继日志的内容,来同步主服务器上的操作。
  • 数据定义语句日志:记录数据定义语句执行的元数据操作。除二进制日志外,其他日志都是文本文件 。默认情况下,所有日志创建于 MySQL数据目录中。

5.1.2 日志的弊端

  • 日志功能会降低MySQL数据库的性能 。
  • 日志会占用大量的磁盘空间 。

5.2 慢查询日志

前面章节《性能分析工具的使用》已经详细讲述。

5.3 通用查询日志

通用查询日志用来记录用户的所有操作 ,包括启动和关闭MySQL服务、所有用户的连接开始时间和截止时间、发给 MySQL数据库服务器的所有SQL指令等。当我们的数据发生异常时,查看通用查询日志,还原操作时的具体场景,可以帮助我们准确定位问题。

5.3.1 问题场景

在电商系统中,购买商品并且使用微信支付完成以后,却发现支付中心的记录并没有新增,此时用户再次使用支付宝支付,就会出现重复支付的问题。但是当去数据库中查询数据的时候,会发现只有一条记录存在。那么此时给到的现象就是只有一条支付记录,但是用户却支付了两次。

我们对系统进行了仔细检查,没有发现数据问题,因为用户编号和订单编号以及第三方流水号都是对的。可是用户确实支付了两次,这个时候,我们想到了检查通用查询日志,看看当天到底发生了什么。

查看之后,发现:1月1日下午2点,用户使用微信支付完以后,但是由于网络故障,支付中心没有及时收到微信支付的回调通知,导致当时没有写入数据。1月1日下午2点30,用户又使用支付宝支付,此时记录更新到支付中心。1月1日晚上 9点,微信的回调通知过来了,但是支付中心已经存在了支付宝的记录,所以只能覆盖记录了。

由于网络的原因导致了重复支付。至于解决问题的方案就很多了,这里省略。

可以看到通用查询日志可以帮助我们了解操作发生的具体时间和操作的细节,对找出异常发生的原因极其关键。

5.3.2 查看当前状态

mysql> SHOW VARIABLES LIKE '%general%';
+------------------+------------------------------+
| Variable_name | Value |
+------------------+------------------------------+
| general_log | OFF | #通用查询日志处于关闭状态
| general_log_file | /var/lib/mysql/atguigu01.log | #通用查询日志文件的名称是atguigu01.log
+------------------+------------------------------+
2 rows in set (0.03 sec)

5.3.3 启动日志

方式1:永久性方式

修改my.cnf或者my.ini配置文件来设置。在[mysqld]组下加入log选项,并重启MySQL服务。格式如下:

[mysqld]
general_log=ON
general_log_file=[path[filename]] #日志文件所在目录路径,filename为日志文件名

如果不指定目录和文件名,通用查询日志将默认存储在MySQL数据目录中的hostname.log文件中,hostname表示主机名。

方式2:临时性方式

SET GLOBAL general_log=on; # 开启通用查询日志
SET GLOBAL general_log_file=’path/filename’; # 设置日志文件保存位置

5.3.4 查看日志

通用查询日志是以文本文件的形式存储在文件系统中的,可以使用文本编辑器直接打开日志文件。每台MySQL服务器的通用查询日志内容是不同的。

在通用查询日志里面,我们可以清楚地看到,什么时候开启了新的客户端登陆数据库,登录之后做了什么 SQL 操作,针对的是哪个数据表等信息。

5.3.5 停止日志

方式1:永久性方式

修改 my.cnf 或者 my.ini 文件,把[mysqld]组下的 general_log 值设置为 OFF 或者把general_log一项注释掉。修改保存后,再 重启MySQL服务 ,即可生效。 举例1:

[mysqld]
general_log=OFF
#general_log=ON

方式2:临时性方式

使用SET语句停止MySQL通用查询日志功能:

SET GLOBAL general_log=off;

5.3.6 删除、刷新日志

如果数据的使用非常频繁,那么通用查询日志会占用服务器非常大的磁盘空间。数据管理员可以删除很长时间之前的查询日志,以保证MySQL服务器上的硬盘空间。

手动删除文件

SHOW VARIABLES LIKE '%general_log%';

可以看出,通用查询日志的目录默认为MySQL数据目录。在该目录下手动删除通用查询日志atguigu01.log。

刷新日志

使用如下命令重新生成查询日志文件,具体命令如下。刷新MySQL数据目录,发现创建了新的日志文件。前提一定要开启通用日志。

mysqladmin -uroot -p flush-logs

5.4 错误日志

5.4.1 启动日志

在MySQL数据库中,错误日志功能是默认开启的。而且,错误日志无法被禁止 。

默认情况下,错误日志存储在MySQL数据库的数据文件夹下,名称默认为 mysqld.log (Linux系统)或hostname.err (mac系统)。如果需要制定文件名,则需要在my.cnf或者my.ini中做如下配置:

[mysqld]
log-error=[path/[filename]] #path为日志文件所在的目录路径,filename为日志文件名

修改配置项后,需要重启MySQL服务以生效。

5.4.2 查看日志

MySQL错误日志是以文本文件形式存储的,可以使用文本编辑器直接查看。

查询错误日志的存储路径:

mysql> SHOW VARIABLES LIKE 'log_err%';
+----------------------------+----------------------------------------+
| Variable_name | Value |
+----------------------------+----------------------------------------+
| log_error | /var/log/mysqld.log |
| log_error_services | log_filter_internal; log_sink_internal |
| log_error_suppression_list | |
| log_error_verbosity | 2 |
+----------------------------+----------------------------------------+
4 rows in set (0.01 sec)

执行结果中可以看到错误日志文件是mysqld.log,位于MySQL默认的数据目录下。

5.4.3 删除、刷新日志

对于很久以前的错误日志,数据库管理员查看这些错误日志的可能性不大,可以将这些错误日志删除,以保证MySQL服务器上的硬盘空间 。

MySQL的错误日志是以文本文件的形式存储在文件系统中的,可以直接删除 。

[root@atguigu01 log]# mysqladmin -uroot -p flush logs

5.5 二进制日志

binlog可以说是MySQL中比较重要的日志了,在日常开发及运维过程中,经常会遇到。

binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的DDL和DML等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等)。

binlog主要应用场景:

  • 一是用于数据恢复
  • 二是用于数据复制

 5.5.1 查看默认情况

查看记录二进制日志是否开启:在MySQL8中默认情况下,二进制文件是开启的。

mysql> show variables like '%log_bin%';
+---------------------------------+------------------------------------+
| Variable_name                   | Value                              |
+---------------------------------+------------------------------------+
| log_bin                         | ON                                 |
| log_bin_basename                | /usr/local/mysql/data/binlog       |
| log_bin_index                   | /usr/local/mysql/data/binlog.index |
| log_bin_trust_function_creators | OFF                                |
| log_bin_use_v1_row_events       | OFF                                |
| sql_log_bin                     | ON                                 |
+---------------------------------+------------------------------------+
6 rows in set (0.00 sec)

5.5.2 日志参数设置

方式1:永久性方式

修改MySQL的my.cnf或my.ini文件可以设置二进制日志的相关参数:

[mysqld]
#启用二进制日志
log-bin=binlog
binlog_expire_logs_seconds=600
max_binlog_size=100M

重新启动MySQL服务,查询二进制日志的信息.

设置带文件夹的bin-log日志存放目录

如果想改变日志文件的目录和名称,可以对my.cnf或my.ini中的log_bin参数修改如下:

[mysqld]
log-bin="/var/lib/mysql/binlog/atguigu-bin"

注意:新建的文件夹需要使用mysql用户,使用下面的命令即可。

chown -R -v mysql:mysql binlog

方式2:临时性方式

如果不希望通过修改配置文件并重启的方式设置二进制日志的话,还可以使用如下指令,需要注意的是在mysql8中只有会话级别的设置,没有了global级别的设置。

# global 级别
mysql> set global sql_log_bin=0;
ERROR 1228 (HY000): Variable 'sql_log_bin' is a SESSION variable and can`t be used with SET GLOBAL
# session级别
mysql> SET sql_log_bin=0;
Query OK, 0 rows affected (0.01 秒)

5.5.3 查看日志

当MySQL创建二进制日志文件时,先创建一个以“filename”为名称、以“.index”为后缀的文件,再创建一个以“filename”为名称、以“.000001”为后缀的文件。

MySQL服务重新启动一次 ,以“.000001”为后缀的文件就会增加一个,并且后缀名按1递增。即日志文件的个数与MySQL服务启动的次数相同;如果日志长度超过max_binlog_size 的上限(默认是1GB),就会创建一个新的日志文件。

查看当前的二进制日志文件列表及大小。指令如下:

mysql> SHOW BINARY LOGS;
+---------------+-----------+-----------+
| Log_name      | File_size | Encrypted |
+---------------+-----------+-----------+
| binlog.000075 |       180 | No        |
| binlog.000076 |       180 | No        |
| binlog.000077 |       157 | No        |
| binlog.000078 |       180 | No        |
| binlog.000079 |       619 | No        |
| binlog.000080 |       180 | No        |
| binlog.000081 |      2764 | No        |
| binlog.000082 |       512 | No        |
| binlog.000083 |       180 | No        |
| binlog.000084 |       789 | No        |
| binlog.000085 |       180 | No        |
| binlog.000086 |       180 | No        |
| binlog.000087 |       157 | No        |
+---------------+-----------+-----------+
13 rows in set (0.00 sec)

下面命令将行事件以 伪SQL的形式 表现出来:

mysqlbinlog -v "/usr/local/mysql/data/binlog.000081"

 前面的命令同时显示binlog格式的语句,使用如下命令不显示它

mysqlbinlog -v --base64-output=DECODE-ROWS "/usr/local/mysql/data/binlog.000081"

关于mysqlbinlog工具的使用技巧还有很多,例如只解析对某个库的操作或者某个时间段内的操作等。简单分享几个常用的语句,更多操作可以参考官方文档。

# 可查看参数帮助
mysqlbinlog --no-defaults --help
# 查看最后100行
mysqlbinlog --no-defaults --base64-output=decode-rows -vv binlog.000081 |tail -100
# 根据position查找
mysqlbinlog --no-defaults --base64-output=decode-rows -vv binlog.000081 |grep -A 20 '2591'

上面这种办法读取出binlog日志的全文内容比较多,不容易分辨查看到pos点信息,下面介绍一种更为方便的查询命令:

mysql> show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count];
  • IN 'log_name' :指定要查询的binlog文件名(不指定就是第一个binlog文件) 
  • FROM pos :指定从哪个pos起始点开始查起(不指定就是从整个文件首个pos点开始算)
  • LIMIT [offset] :偏移量(不指定就是0)
  • row_count :查询总条数(不指定就是所有行)
mysql> show binlog events IN 'binlog.000081' from 2591;
+---------------+------+-------------+-----------+-------------+---------------------------------+
| Log_name      | Pos  | Event_type  | Server_id | End_log_pos | Info                            |
+---------------+------+-------------+-----------+-------------+---------------------------------+
| binlog.000081 | 2591 | Table_map   |         1 |        2648 | table_id: 222 (atguigudb.test)  |
| binlog.000081 | 2648 | Update_rows |         1 |        2710 | table_id: 222 flags: STMT_END_F |
| binlog.000081 | 2710 | Xid         |         1 |        2741 | COMMIT /* xid=339 */            |
| binlog.000081 | 2741 | Stop        |         1 |        2764 |                                 |
+---------------+------+-------------+-----------+-------------+---------------------------------+
4 rows in set (0.00 sec)

上面我们讲了这么多都是基于binlog的默认格式,binlog格式查看

mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+
1 row in set (0.00 sec)

除此之外,binlog还有2种格式,分别是Statement和Mixed

  • Statement
    • 每一条会修改数据的sql都会记录在binlog中。
    • 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。
  • Row
    • 5.1.5版本的MySQL才开始支持row level 的复制,它不记录sql语句上下文相关信息,仅保存哪条记录被修改。
    • 优点:row level 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题。
  • Mixed
    • 从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。

5.5.4 使用日志恢复数据

mysqlbinlog恢复数据的语法如下:

mysqlbinlog [option] filename|mysql –uuser -ppass;

mysqlbinlog --start-date='2023-03-15 15:00:00'  --stop-date='2023-03-15 15:00:00' /usr/local/mysql/data/binlog.000081 | mysql -uroot -p 

这个命令可以这样理解:使用mysqlbinlog命令来读取filename中的内容,然后使用mysql命令将这些内容恢复到数据库中。

  • filename :是日志文件名。
  • option :可选项,比较重要的两对option参数是--start-date、--stop-date 和 --start-position、--stop-position。
    • --start-date 和 --stop-date :可以指定恢复数据库的起始时间点和结束时间点。
    • --start-position和--stop-position :可以指定恢复数据的开始位置和结束位置。

注意:使用mysqlbinlog命令进行恢复操作时,必须是编号小的先恢复,例如binlog.000001必须在binlog.000002之前恢复。

5.5.5 删除二进制日志

MySQL的二进制文件可以配置自动删除,同时MySQL也提供了安全的手动删除二进制文件的方法。

PURGE MASTER LOGS 只删除指定部分的二进制日志文件,

RESET MASTER 删除所有的二进制日志文件。

1、PURGE MASTER LOGS:删除指定日志文件

PURGE MASTER LOGS语法如下:

PURGE {MASTER | BINARY} LOGS TO ‘指定日志文件名’;
PURGE {MASTER | BINARY} LOGS BEFORE ‘指定日期’;

2、RESET MASTER: 删除所有二进制日志文件

慎用!

RESET MASTER;

执行完该语句后,原来的所有二进制日志已经全部被删除。

5.5.6 其他场景

二进制日志可以通过数据库的全量备份和二进制日志中保存的增量信息 ,完成数据库的 无损失恢复 。但是,如果遇到数据量大、数据库和数据表很多(比如分库分表的应用)的场景,用二进制日志进行数据恢复,是很有挑战性的,因为起止位置不容易管理。在这种情况下,一个有效的解决办法是配置主从数据库服务器 ,甚至是一主多从的架构,把二进制日志文件的内容通过中继日志,同步到从数据库服务器中,这样就可以有效避免数据库故障导致的数据异常等问题。

5.6 再谈二进制日志

5.6.1 写入机制

binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache ,事务提交的时候,再把binlog cache写到binlog文件中。因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。

 write和fsync的时机,可以由参数sync_binlog控制,默认是0。为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。

虽然性能得到提升,但是机器宕机,page cache里面的binglog 会丢失。如下图:

 为了安全起见,可以设置为 1 ,表示每次提交事务都会执行fsync,就如同redo log 刷盘流程一样。

最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync。

 在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N个事务的binlog日志。

5.6.2 binlog与redolog对比

  • redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎层产生的。
  • 而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。

5.6.3 两阶段提交

在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的写入时机不一样。

 redo log与binlog两份日志之间的逻辑不一致,会出现什么问题?

 由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。

 为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。

 使用两阶段提交后,写入binlog时发生异常也不会有影响

 另一个场景,redo log设置commit阶段发生异常,那会不会回滚事务呢?

 并不会回滚事务,它会执行上图框住的逻辑,虽然redo log是处于prepare阶段,但是能通过事务id找到对应的binlog日志,所以MySQL认为是完整的,就会提交事务恢复数据。

5.7 中继日志

5.7.1 介绍

中继日志只在主从服务器架构的从服务器上存在。从服务器为了与主服务器保持一致,要从主服务器读取二进制日志的内容,并且把读取到的信息写入本地的日志文件中,这个从服务器本地的日志文件就叫中继日志 。然后,从服务器读取中继日志,并根据中继日志的内容对从服务器的数据进行更新,完成主从服务器的数据同步 。

搭建好主从服务器之后,中继日志默认会保存在从服务器的数据目录下。

文件名的格式是: 从服务器名-relay-bin.序号。中继日志还有一个索引文件: 从服务器名-relaybin.index,用来定位当前正在使用的中继日志。

5.7.2 查看中继日志

中继日志与二进制日志的格式相同,可以用mysqlbinlog工具进行查看。下面是中继日志的一个片段:

SET TIMESTAMP=1618558728/*!*/;
BEGIN
/*!*/;
# at 950
#210416 15:38:48 server id 1 end_log_pos 832 CRC32 0xcc16d651 Table_map:
`atguigu`.`test` mapped to number 91
# at 1000
#210416 15:38:48 server id 1 end_log_pos 872 CRC32 0x07e4047c Delete_rows: table id
91 flags: STMT_END_F -- server id 1 是主服务器,意思是主服务器删了一行数据
BINLOG '
CD95YBMBAAAAMgAAAEADAAAAAFsAAAAAAAEABGRlbW8ABHRlc3QAAQMAAQEBAFHWFsw=
CD95YCABAAAAKAAAAGgDAAAAAFsAAAAAAAEAAgAB/wABAAAAfATkBw==
'/*!*/;
# at 1040

这一段的意思是,主服务器(“server id 1”)对表atguigu.test进行了2步操作:

定位到表 atguigu.test 编号是 91 的记录,日志位置是 832;
删除编号是 91 的记录,日志位置是 872。

5.7.3 恢复的典型错误

如果从服务器宕机,有的时候为了系统恢复,要重装操作系统,这样就可能会导致你的服务器名称与之前不同 。而中继日志里是包含从服务器名的。在这种情况下,就可能导致你恢复从服务器的时候,无法从宕机前的中继日志里读取数据,以为是日志文件损坏了,其实是名称不对了。

解决的方法也很简单,把从服务器的名称改回之前的名称。

六、主从复制

6.1 主从复制概述

6.1.2 如何提升数据库并发能力

  • 考虑将热点数据缓存:

  • 做主从架构 、进行读写分离。

一般应用对数据库而言都是“ 读多写少 ”,也就说对数据库读取数据的压力比较大,有一个思路就是采用数据库集群的方案,做主从架构 、进行读写分离 ,这样同样可以提升数据库的并发处理能力。

但并不是所有的应用都需要对数据库进行主从架构的设置,毕竟设置架构本身是有成本的。

如果目的在于提升数据库高并发访问的效率,那么首先考虑的是如何优化SQL和索引 ,这种方式简单有效;其次才是采用缓存的策略 ,比如使用 Redis将热点数据保存在内存数据库中,提升读取的效率;最后才是对数据库采用主从架构 ,进行读写分离。

6.1.3 主从复制的作用

主从同步设计不仅可以提高数据库的吞吐量,还有以下 3 个方面的作用。

  • 读写分离。
  • 数据备份。
  • 具有高可用性。

6.2 主从复制原理

Slave 会从 Master 读取 binlog 来进行数据同步。

6.2.1 原理剖析

6.2.1.1三个线程

实际上主从同步的原理就是基于binlog进行数据同步的。在主从复制过程中,会基于3个线程来操作,一个主库线程,两个从库线程。

 二进制日志转储线程(Binlog dump thread)是一个主库线程。当从库线程连接的时候, 主库可以将二进制日志发送给从库,当主库读取事件(Event)的时候,会在Binlog上加锁 ,读取完成之后,再将锁释放掉。

从库I/O 线程会连接到主库,向主库发送请求更新Binlog。这时从库的I/O线程就可以读取到主库的二进制日志转储线程发送的Binlog更新部分,并且拷贝到本地的中继日志 (Relay log)。

从库SQL线程 会读取从库中的中继日志,并且执行日志中的事件,将从库中的数据与主库保持同步。

6.2.1.2复制三步骤

  • 步骤1:Master将写操作记录到二进制日志( binlog )。
  • 步骤2:Slave将Master的binary log events拷贝到它的中继日志( relay log );
  • 步骤3:Slave重做中继日志中的事件,将改变应用到自己的数据库中。 MySQL复制是异步的且串行化的,而且重启后从接入点开始复制。

6.2.1.3复制的问题

复制的最大问题: 延时

6.2.2 复制的基本原则

  • 每个Slave只有一个Master
  • 每个Slave只能有一个唯一的服务器ID
  • 每个Master可以有多个Slave

6.3 一主一从架构搭建

一台主机用于处理所有写请求 ,一台从机负责所有读请求 ,架构图如下:

6.3.1 准备工作

1、准备 2台 CentOS 虚拟机

2、每台虚拟机上需要安装好MySQL (可以是MySQL8.0 )

注意:克隆的方式需要修改新克隆出来主机的:① MAC地址 ② hostname ③ IP 地址 ④ UUID 。

此外,克隆的方式生成的虚拟机(包含MySQL Server),则克隆的虚拟机MySQL Server的UUID相同,必须修改,否则在有些场景会报错。比如: show slave status\G ,报如下的错误:

Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave haveequal MySQL server UUIDs; these UUIDs must be different for replication to work.

修改MySQL Server 的UUID方式:

vim /var/lib/mysql/auto.cnf systemctl restart mysqld

6.3.2 主机配置文件

建议mysql版本一致且后台以服务运行,主从所有配置项都配置在 [mysqld] 节点下,且都是小写字母。

具体参数配置如下:

  • 必选
#[必须]主服务器唯一ID
server-id=1

#[必须]启用二进制日志,指名路径。比如:自己本地的路径/log/mysqlbin
log-bin=bin-log
  • 可选
#[可选] 0(默认)表示读写(主机),1表示只读(从机)
read-only=0
#设置日志文件保留的时长,单位是秒
binlog_expire_logs_seconds=6000
#控制单个二进制日志大小。此参数的最大和默认值是1GB
max_binlog_size=200M
#[可选]设置不要复制的数据库
binlog-ignore-db=test
#[可选]设置需要复制的数据库,默认全部记录。
binlog-do-db=需要复制的主数据库名字
#[可选]设置binlog格式
binlog_format=STATEMENT

binlog格式设置:

1、STATEMENT模式 (基于SQL语句的复制(statement-based replication, SBR))

binlog_format=STATEMENT

每一条会修改数据的sql语句会记录到binlog中。这是默认的binlog格式。

  • SBR 的优点:
    • 历史悠久,技术成熟
    • 不需要记录每一行的变化,减少了binlog日志量,文件较小
    • binlog中包含了所有数据库更改信息,可以据此来审核数据库的安全等情况
    • binlog可以用于实时的还原,而不仅仅用于复制
    • 主从版本可以不一样,从服务器版本可以比主服务器版本高
  • SBR 的缺点:
    • 不是所有的UPDATE语句都能被复制,尤其是包含不确定操作的时候
  • 使用以下函数的语句也无法被复制:LOAD_FILE()、UUID()、USER()、FOUND_ROWS()、SYSDATE()(除非启动时启用了 --sysdate-is-now 选项)
  • INSERT ... SELECT 会产生比 RBR 更多的行级锁
  • 复制需要进行全表扫描(WHERE 语句中没有使用到索引)的 UPDATE 时,需要比 RBR 请求更多的行级锁
  • 对于有 AUTO_INCREMENT 字段的 InnoDB表而言,INSERT 语句会阻塞其他 INSERT 语句
  • 对于一些复杂的语句,在从服务器上的耗资源情况会更严重,而 RBR 模式下,只会对那个发生变化的记录产生影响
  • 执行复杂语句如果出错的话,会消耗更多资源
  • 数据表必须几乎和主服务器保持一致才行,否则可能会导致复制出错

2、ROW模式(基于行的复制(row-based replication, RBR))

binlog_format=ROW
  • RBR 的优点:
    • 任何情况都可以被复制,这对复制来说是最安全可靠的。(比如:不会出现某些特定情况下的存储过程、function、trigger的调用和触发无法被正确复制的问题)
    • 多数情况下,从服务器上的表如果有主键的话,复制就会快了很多
    • 复制以下几种语句时的行锁更少:INSERT ... SELECT、包含 AUTO_INCREMENT 字段的 INSERT、没有附带条件或者并没有修改很多记录的 UPDATE 或 DELETE 语句
    • 执行 INSERT,UPDATE,DELETE 语句时锁更少
    • 从服务器上采用多线程来执行复制成为可能
  • RBR 的缺点:
    • binlog 大了很多
    • 复杂的回滚时 binlog 中会包含大量的数据
    • 主服务器上执行UPDATE语句时,所有发生变化的记录都会写到binlog中,而 SBR 只会写一次,这会导致频繁发生 binlog 的并发写问题
    • 无法从binlog中看到都复制了些什么语句

3、MIXED模式(混合模式复制(mixed-based replication, MBR))

binlog_format=MIXED

从5.1.8版本开始,MySQL提供了Mixed格式,实际上就是Statement与Row的结合。

binlog_format=STATEMENTbinlog_format=ROWbinlog_format=MIXED在Mixed模式下,一般的语句修改使用statment格式保存binlog。

如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog。MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。

6.3.3 从机配置文件

要求主从所有配置项都配置在 my.cnf 的 [mysqld] 栏位下,且都是小写字母。

  • 必选
#[必须]从服务器唯一ID
server-id=2
  • 可选
#[可选]启用中继日志
relay-log=mysql-relay

重启后台mysql服务,使配置生效。

注意:主从机都关闭防火墙

service iptables stop #CentOS 6

systemctl stop firewalld.service #CentOS 7

6.3.4 主机:建立账户并授权

#5.5,5.7
#在主机MySQL里执行授权主从复制的命令
GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'从机器数据库IP' IDENTIFIED BY 'abc123';

注意:如果使用的是MySQL8,需要如下的方式建立账户,并授权slave:

CREATE USER 'slave1'@'%' IDENTIFIED BY '123456';
GRANT REPLICATION SLAVE ON *.* TO 'slave1'@'%';
#此语句必须执行。否则见下面。
ALTER USER 'slave1'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
flush privileges;

注意:在从机执行show slave status\G时报错:

Last_IO_Error: error connecting to master '[email protected]:3306' - retry-time: 60 retries: 1

message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.

查询Master的状态,并记录下File和Position的值。

show master status;

6.3.5 从机:配置需要复制的主机

步骤1:从机上复制主机的命令

CHANGE MASTER TO
MASTER_HOST='主机的IP地址',
MASTER_USER='主机用户名',
MASTER_PASSWORD='主机用户名的密码',
MASTER_LOG_FILE='mysql-bin.具体数字',
MASTER_LOG_POS=具体值;

举例:

CHANGE MASTER TO
MASTER_HOST='192.168.1.150',MASTER_USER='slave1',MASTER_PASSWORD='123456',MASTER_LOG_F
ILE='atguigu-bin.000007',MASTER_LOG_POS=154;

步骤2:

#启动slave同步
START SLAVE;

 如果报错:

 可以执行如下操作,删除之前的relay_log信息。然后重新执行 CHANGE MASTER TO ...语句即可。

mysql> reset slave; 
#删除SLAVE数据库的relaylog日志文件,并重新启用新的relaylog文件

接着,查看同步状态:

SHOW SLAVE STATUS\G;

 显式如下的情况,就是不正确的。可能错误的原因有:

  • 网络不通
  • 账户密码错误
  • 防火墙
  • mysql配置文件问题
  • 连接服务器时语法
  • 主服务器mysql权限

6.3.6 测试

主机新建库、新建表、insert记录,从机复制:

CREATE DATABASE atguigu_master_slave;
CREATE TABLE mytbl(id INT,NAME VARCHAR(16));
INSERT INTO mytbl VALUES(1, 'zhang3');
INSERT INTO mytbl VALUES(2,@@hostname);

6.3.7 停止主从同步

  • 停止主从同步命令
stop slave;
  • 如何重新配置主从

如果停止从服务器复制功能,再使用需要重新配置主从。

重新配置主从,需要在从机上执行:

stop slave;
reset master; 
#删除Master中所有的binglog文件,并将日志索引文件清空,重新开始所有新的日志文件(慎用)

6.3.8 后续

搭建主从复制:双主双从

6.4 同步数据一致性问题

主从同步的要求:

  • 读库和写库的数据一致(最终一致);
  • 写数据必须写到写库;
  • 读数据必须到读库(不一定);

6.4.1 理解主从延迟问题

进行主从同步的内容是二进制日志,它是一个文件,在进行网络传输的过程中就一定会存在主从延迟(比如 500ms),这样就可能造成用户在从库上读取的数据不是最新的数据,也就是主从同步中的数据不一致性问题。

6.4.2 主从延迟问题原因

在网络正常的时候,日志从主库传给从库所需的时间是很短的,即T2-T1的值是非常小的。即,网络正常情况下,主备延迟的主要来源是备库接收完binlog和执行完这个事务之间的时间差。主备延迟最直接的表现是,从库消费中继日志(relay log)的速度,比主库生产binlog的速度要慢。造成原因:

1、从库的机器性能比主库要差

2、从库的压力大

3、大事务的执行

举例1:一次性用delete语句删除太多数据结论:后续再删除数据的时候,要控制每个事务删除的数据量,分成多次删除。

举例2:一次性用insert...select插入太多数据

举例3:大表DDL

比如在主库对一张500W的表添加一个字段耗费了10分钟,那么从节点上也会耗费10分钟。

6.4.3 如何减少主从延迟

若想要减少主从延迟的时间,可以采取下面的办法:

  1. 降低多线程大事务并发的概率,优化业务逻辑
  2. 优化SQL,避免慢SQL, 减少批量操作 ,建议写脚本以update-sleep这样的形式完成。
  3. 提高从库机器的配置 ,减少主库写binlog和从库读binlog的效率差。
  4. 尽量采用短的链路 ,也就是主库和从库服务器的距离尽量要短,提升端口带宽,减少binlog传输的网络延时。
  5. 实时性要求的业务读强制走主库,从库只做灾备,备份。

6.4.4 如何解决一致性问题

如果操作的数据存储在同一个数据库中,那么对数据进行更新的时候,可以对记录加写锁,这样在读取的时候就不会发生数据不一致的情况。但这时从库的作用就是备份 ,并没有起到读写分离 ,分担主库读压力的作用。

 读写分离情况下,解决主从同步中数据不一致的问题, 就是解决主从之间数据复制方式的问题,如果按照数据一致性从弱到强来进行划分,有以下 3 种复制方式。

方法 1:异步复制

 方法 2:半同步复制

 方法 3:组复制

异步复制和半同步复制都无法最终保证数据的一致性问题,半同步复制是通过判断从库响应的个数来决定是否返回给客户端,虽然数据一致性相比于异步复制有提升,但仍然无法满足对数据一致性要求高的场景,比如金融领域。MGR 很好地弥补了这两种复制模式的不足。组复制技术,简称 MGR(MySQL Group Replication)。是 MySQL 在 5.7.17 版本中推出的一种新的数据复制技术,这种复制技术是基于 Paxos 协议的状态机复制。

MGR 是如何工作的

首先我们将多个节点共同组成一个复制组,在 执行读写(RW)事务的时候,需要通过一致性协议层(Consensus 层)的同意,也就是读写事务想要进行提交,必须要经过组里“大多数人”(对应 Node 节点)的同意,大多数指的是同意的节点数量需要大于 (N/2+1),这样才可以进行提交,而不是原发起方一个说了算。而针对只读(RO)事务 则不需要经过组内同意,直接COMMIT即可。在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消息和全局有序消息,从而保证组内数据的一致性。

 MGR 将MySQL带入了数据强一致性的时代,是一个划时代的创新,其中一个重要的原因就是MGR 是基于Paxos协议的。Paxos算法是由Leslie Lamport于1990年提出的,有关这个算法的决策机制可以搜一下。事实上,Paxos算法提出来之后就作为分布式一致性算法被广泛应用,比如Apache的 ZooKeeper 也是基于Paxos实现的。

6.5 知识延伸

在主从架构的配置中,如果想要采取读写分离的策略,我们可以自己编写程序 ,也可以通过第三方的中间件来实现。

  • 自己编写程序的好处就在于比较自主,我们可以自己判断哪些查询在从库上来执行,针对实时性要求高的需求,我们还可以考虑哪些查询可以在主库上执行。同时,程序直接连接数据库,减少了中间件层,相当于减少了性能损耗。
  • 采用中间件的方法有很明显的优势, 功能强大 , 使用简单 。但因为在客户端和数据库之间增加了中间件层会有一些性能损耗 ,同时商业中间件也是有使用成本的。我们也可以考虑采取一些优秀的开源工具。

  1. Cobar 属于阿里B2B事业群,始于2008年,在阿里服役3年多,接管3000+个MySQL数据库的schema,集群日处理在线SQL请求50亿次以上。由于Cobar发起人的离职,Cobar停止维护。
  2. Mycat 是开源社区在阿里cobar基础上进行二次开发,解决了cobar存在的问题,并且加入了许多新的功能在其中。青出于蓝而胜于蓝。
  3. OneProxy 基于MySQL官方的proxy思想利用c语言进行开发的,OneProxy是一款商业 收费的中间件。舍弃了一些功能,专注在性能和稳定性上 。
  4. kingshard 由小团队用go语言开发,还需要发展,需要不断完善。
  5. Vitess 是Youtube生产在使用,架构很复杂。不支持MySQL原生协议,使用需要大量改造成本 。
  6. Atlas 是360团队基于mysql proxy改写,功能还需完善,高并发下不稳定。
  7. MaxScale 是mariadb(MySQL原作者维护的一个版本)研发的中间件
  8. MySQLRoute 是MySQL官方Oracle公司发布的中间件

 主备切换:

七、数据库备份与恢复

7.1 物理备份和逻辑备份

物理备份:备份数据文件,转储数据库物理文件到某一目录。物理备份恢复速度比较快,但占用空间比较大,MySQL中可以用xtrabackup工具来进行物理备份。

逻辑备份:对数据库对象利用工具进行导出工作,汇总入备份文件内。逻辑备份恢复速度慢,但占用空间小,更灵活。MySQL 中常用的逻辑备份工具为mysqldump 。逻辑备份就是备份sql语句 ,在恢复的时候执行备份的sql语句实现数据库数据的重现。

7.2 mysqldump实现逻辑备份

7.2.1 备份一个数据库

基本语法:

mysqldump –u 用户名称 –h 主机名称 –p密码 待备份的数据库名称[tbname, [tbname...]]> 备份文件名称.sql

举例:

mysqldump -uroot -p xiang > test.sql #备份文件存储在当前目录下
mysqldump -uroot -p xiang > /var/lib/mysql/xiang.sql

7.2.2 备份全部数据库

若想用mysqldump备份整个实例,可以使用 --all-databases 或 -A 参数:

mysqldump -uroot -pxxxxxx --all-databases > all_database.sql
mysqldump -uroot -pxxxxxx -A > all_database.sql

7.2.3 备份部分数据库

使用 --databases 或 -B 参数了,该参数后面跟数据库名称,多个数据库间用空格隔开。如果指定databases参数,备份文件中会存在创建数据库的语句,如果不指定参数,则不存在。语法如下:

mysqldump –u user –h host –p --databases [数据库的名称1 [数据库的名称2...]] > 备份文件名称.sql

举例:

mysqldump -uroot -p --databases xiang test > two_database.sql
mysqldump -uroot -p -B xiang test > two_database.sql

如果不携带--database,则仅支持导出一个数据库

7.2.4 备份部分表

比如,在表变更前做个备份。语法如下:

mysqldump –u user –h host –p 数据库的名称 [表名1 [表名2...]] > 备份文件名称.sql

举例:

mysqldump -uroot -p xiang test > test2.sql
mysqldump -uroot -p xiang test emp1 > test2.sql

7.2.5 备份单表的部分数据

有些时候一张表的数据量很大,我们只需要部分数据。这时就可以使用 --where 选项了。where后面附带需要满足的条件。

举例:备份student表中id小于10的数据:

mysqldump -uroot -p xiang student --where="id < 10 " > student_part_id10_low_bak.sql

7.2.6 排除某些表的备份

如果我们想备份某个库,但是某些表数据量很大或者与业务关联不大,这个时候可以考虑排除掉这些表,同样的,选项 --ignore-table 可以完成这个功能。

mysqldump -uroot -p -B test xiang --ignore-table=test.student > no_stu_bak.sql

通过如下指定判定文件中没有student表结构:

grep "student" no_stu_bak.sql

7.2.7 只备份结构或只备份数据

只备份结构的话可以使用 --no-data 简写为 -d 选项;只备份数据可以使用 --no-create-info 简写为-t 选项。

# 只备份结构
mysqldump -uroot -p xiang --no-data > no_data_bak.sql

# 只备份数据
mysqldump -uroot -p xiang --no-create-info > no_create_info_bak.sql

7.2.8 备份中包含存储过程、函数、事件

mysqldump备份默认是不包含存储过程,自定义函数及事件的。可以使用 --routines 或 -R 选项来备份存储过程及函数,使用 --events 或 -E 参数来备份事件。

举例:备份整个库,包含存储过程及事件:

使用下面的SQL可以查看当前库有哪些存储过程或者函数

SELECT SPECIFIC_NAME,ROUTINE_TYPE ,ROUTINE_SCHEMA FROM information_schema.Routines WHERE ROUTINE_SCHEMA="xiang";

下面备份atguigu库的数据,函数以及存储过程。

mysqldump -uroot -p -R -E --databases xiang > full_bak.sql

7.2.9 mysqldump常用选项

--add-drop-database:在每个CREATE DATABASE语句前添加DROP DATABASE语句。
--add-drop-tables:在每个CREATE TABLE语句前添加DROP TABLE语句。
--add-locking:用LOCK TABLES和UNLOCK TABLES语句引用每个表转储。重载转储文件时插入得更快。
--all-database, -A:转储所有数据库中的所有表。与使用--database选项相同,在命令行中命名所有数据库。
--comment[=0|1]:如果设置为0,禁止转储文件中的其他信息,例如程序版本、服务器版本和主机。--skipcomments与--comments=0的结果相同。默认值为1,即包括额外信息。
--compact:产生少量输出。该选项禁用注释并启用--skip-add-drop-tables、--no-set-names、--skipdisable-keys和--skip-add-locking选项。
--compatible=name:产生与其他数据库系统或旧的MySQL服务器更兼容的输出,值可以为ansi、MySQL323、MySQL40、postgresql、oracle、mssql、db2、maxdb、no_key_options、no_table_options或者no_field_options。
--complete_insert, -c:使用包括列名的完整的INSERT语句。
--debug[=debug_options], -#[debug_options]:写调试日志。
--delete,-D:导入文本文件前清空表。
--default-character-set=charset:使用charsets默认字符集。如果没有指定,就使用utf8。
--delete--master-logs:在主复制服务器上,完成转储操作后删除二进制日志。该选项自动启用-masterdata。
--extended-insert,-e:使用包括几个VALUES列表的多行INSERT语法。这样使得转储文件更小,重载文件时可以加速插入。
--flush-logs,-F:开始转储前刷新MySQL服务器日志文件。该选项要求RELOAD权限。
--force,-f:在表转储过程中,即使出现SQL错误也继续。
--lock-all-tables,-x:对所有数据库中的所有表加锁。在整体转储过程中通过全局锁定来实现。该选项自动关闭--single-transaction和--lock-tables。
--lock-tables,-l:开始转储前锁定所有表。用READ LOCAL锁定表以允许并行插入MyISAM表。对于事务表(例如InnoDB和BDB),--single-transaction是一个更好的选项,因为它根本不需要锁定表。
--no-create-db,-n:该选项禁用CREATE DATABASE /*!32312 IF NOT EXIST*/db_name语句,如果给出--database或--all-database选项,就包含到输出中。
--no-create-info,-t:只导出数据,而不添加CREATE TABLE语句。
--no-data,-d:不写表的任何行信息,只转储表的结构。
--opt:该选项是速记,它可以快速进行转储操作并产生一个能很快装入MySQL服务器的转储文件。该选项默认开启,但可以用--skip-opt禁用。
--password[=password],-p[password]:当连接服务器时使用的密码。-port=port_num,-P port_num:用于连接的TCP/IP端口号。
--protocol={TCP|SOCKET|PIPE|MEMORY}:使用的连接协议。
--replace,-r –replace和--ignore:控制替换或复制唯一键值已有记录的输入记录的处理。如果指定--replace,新行替换有相同的唯一键值的已有行;如果指定--ignore,复制已有的唯一键值的输入行被跳过。如果不指定这两个选项,当发现一个复制键值时会出现一个错误,并且忽视文本文件的剩余部分。
--silent,-s:沉默模式。只有出现错误时才输出。
--socket=path,-S path:当连接localhost时使用的套接字文件(为默认主机)。
--user=user_name,-u user_name:当连接服务器时MySQL使用的用户名。
--verbose,-v:冗长模式,打印出程序操作的详细信息。
--xml,-X:产生XML输出。

7.3 mysql命令恢复数据

基本语法:

mysql –u root –p [dbname] < backup.sql

7.3.1 单库备份中恢复单库

使用root用户,将之前练习中备份的atguigu.sql文件中的备份导入数据库中,命令如下:如果备份文件中包含了创建数据库的语句,则恢复的时候不需要指定数据库名称,如下所示

mysql -uroot -p < test.sql

否则需要指定数据库名称,如下所示

mysql -uroot -p xiang < test.sql

7.3.2 全量备份恢复

如果我们现在有昨天的全量备份,现在想整个恢复,则可以这样操作:

mysql –u root –p < all.sql

执行完后,MySQL数据库中就已经恢复了all.sql文件中的所有数据库。

7.3.3 从全量备份中恢复单库

可能有这样的需求,比如说我们只想恢复某一个库,但是我们有的是整个实例的备份,这个时候我们可以从全量备份中分离出单个库的备份。

举例:

sed -n '/^-- Current Database: `xiang`/,/^-- Current Database: `/p' all_database.sql> xiang.sql
#分离完成后我们再导入xiang.sql即可恢复单个库

7.3.4 从单库备份中恢复单表

这个需求还是比较常见的。比如说我们知道哪个表误操作了,那么就可以用单表恢复的方式来恢复。

举例:我们有xiang整库的备份,但是由于class表误操作,需要单独恢复出这张表。

cat xiang.sql | sed -e '/./{H;$!d;}' -e 'x;/CREATE TABLE `class`/!d;q' > class_structure.sql
cat xiang.sql | grep --ignore-case 'insert into `class`' > class_data.sql
#用shell语法分离出创建表的语句及插入数据的语句后 再依次导出即可完成恢复
use xiang;
mysql> source class_structure.sql;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> source class_data.sql;
Query OK, 1 row affected (0.01 sec)

7.4 物理备份:直接复制整个数据库

直接将MySQL中的数据库文件复制出来。这种方法最简单,速度也最快。

MySQL的数据库目录位置不一定相同:

  • 在Windows平台下,MySQL 8.0存放数据库的目录通常默认为 “ C:\ProgramData\MySQL\MySQLServer 8.0\Data ”或者其他用户自定义目录;
  • 在Linux平台下,数据库目录位置通常为/var/lib/mysql/;
  • 在MAC OSX平台下,数据库目录位置通常为“/usr/local/mysql/data”

但为了保证备份的一致性。需要保证:

  1. 方式1:备份前,将服务器停止。
  2. 方式2:备份前,对相关表执行 FLUSH TABLES WITH READ LOCK 操作。这样当复制数据库目录中的文件时,允许其他客户继续查询表。同时,FLUSH TABLES语句来确保开始备份前将所有激活的索引页写入硬盘。

这种方式方便、快速,但不是最好的备份方法,因为实际情况可能 不允许停止MySQL服务器 或者 锁住表 ,而且这种方法 对InnoDB存储引擎 的表不适用。对于MyISAM存储引擎的表,这样备份和还原很方便,但是还原时最好是相同版本的MySQL数据库,否则可能会存在文件类型不同的情况。

注意,物理备份完毕后,执行 UNLOCK TABLES 来结算其他客户对表的修改行为。说明: 在MySQL版本号中,第一个数字表示主版本号,主版本号相同的MySQL数据库文件格式相同。

此外,还可以考虑使用相关工具实现备份。比如, MySQLhotcopy 工具。MySQLhotcopy是一个Perl脚本,它使用LOCK TABLES、FLUSH TABLES和cp或scp来快速备份数据库。它是备份数据库或单个表最快的途径,但它只能运行在数据库目录所在的机器上,并且只能备份MyISAM类型的表。多用于mysql5.5之前。

7.5 物理恢复:直接复制到数据库目录

步骤:

1)演示删除备份的数据库中指定表的数据

2)将备份的数据库数据拷贝到数据目录下,并重启MySQL服务器

3)查询相关表的数据是否恢复。需要使用下面的 chown 操作。

要求:

  • 必须确保备份数据的数据库和待恢复的数据库服务器的主版本号相同。因为只有MySQL数据库主版本号相同时,才能保证这两个MySQL数据库文件类型是相同的。
  • 这种方式对 MyISAM类型的表比较有效 ,对于InnoDB类型的表则不可用。因为InnoDB表的表空间不能直接复制。
  • 在Linux操作系统下,复制到数据库目录后,一定要将数据库的用户和组变成mysql,命令如下:
chown -R mysql.mysql /var/lib/mysql/dbname

其中,两个mysql分别表示组和用户;“-R”参数可以改变文件夹下的所有子文件的用户和组;“dbname”参数表示数据库目录。

提示 Linux操作系统下的权限设置非常严格。通常情况下,MySQL数据库只有root用户和mysql用户组下的mysql用户才可以访问,因此将数据库目录复制到指定文件夹后,一定要使用chown命令将文件夹的用户组变为mysql,将用户变为mysql。

7.6 表的导出和导入

7.6.1 表的导出

7.6.1.1 使用SELECT...INTO OUTFILE

在MySQL中,可以使用SELECT…INTO OUTFILE语句将表的内容导出成一个文本文件。

举例:使用SELECT…INTO OUTFILE将xiang数据库中test表中的记录导出到文本文件。

(1)选择数据库xiang,并查询test表,执行结果如下所示。

mysql> use xiang;
Database changed
mysql> select * from test;
+------+------+----------+------------+
| id   | name | salary   | hire_date  |
+------+------+----------+------------+
|    1 | Tom  | 10000.00 | 2022-06-01 |
|    3 | Tom2 | 20000.00 | 2022-06-02 |
+------+------+----------+------------+
2 rows in set (0.00 sec)

(2)mysql默认对导出的目录有权限限制,也就是说使用命令行进行导出的时候,需要指定目录进行操作。

# 查询secure_file_priv值:
mysql> SHOW GLOBAL VARIABLES LIKE '%secure%';
+--------------------------+-----------------------+
| Variable_name | Value |
+--------------------------+-----------------------+
| require_secure_transport | OFF |
| secure_file_priv | /var/lib/mysql-files/ |
+--------------------------+-----------------------+
2 rows in set (0.02 sec)

(3)上面结果中显示,secure_file_priv变量的值为/var/lib/mysql-files/,导出目录设置为该目录,SQL语句如下。

SELECT * FROM test INTO OUTFILE "/var/lib/mysql-files/test.txt";

(4)查看 /var/lib/mysql-files/test.txt`文件。

7.6.1.2 使用mysqldump命令导出文本文件

举例1:使用mysqldump命令将将xiang数据库中tmp1表中的记录导出到文本文件:

mysqldump -uroot -p -T "/var/lib/mysql-files/" xiang test

mysqldump命令执行完毕后,在指定的目录/var/lib/mysql-files/下生成了test.sql和test.txt文件。

test.sql文件,其内容包含创建表的CREATE语句。打开test.txt文件,其内容只包含表中的数据。

举例2:使用mysqldump将atguigu数据库中的account表导出到文本文件,使用FIELDS选项,要求字段之间使用逗号“,”间隔,所有字符类型字段值用双引号括起来:

mysqldump -uroot -p -T "/var/lib/mysql-files/" atguigu account --fields-terminatedby=',' --fields-optionally-enclosed-by='\"'

7.6.1.3 使用mysql命令导出文本文件

举例1:使用mysql语句导出xiang数据中test表中的记录到文本文件:

mysql -uroot -p --execute="SELECT * FROM test;" xiang > "/var/lib/mysqlfiles/test31.txt"

打开test.txt文件,其内容只包含表中的数据。

举例2:将表中的记录导出到文本文件,使用--veritcal参数将该条件记录分为多行显示:

mysql -uroot -p --vertical --execute="SELECT * FROM test;" xiang > "/var/lib/mysql-files/test32.txt"

举例3:将表中的记录导出到xml文件,使用--xml参数,具体语句如下。

mysql -uroot -p --xml --execute="SELECT * FROM test;" xiang > "/var/lib/mysqlfiles/test33.xml"

7.6.2 表的导入

7.6.2.1 使用LOAD DATA IN FILE

举例1:

使用SELECT...INTO OUTFILE将xiang数据库中test表的记录导出到文本文件

SELECT * FROM xiang.test INTO OUTFILE '/var/lib/mysql-files/test.txt';

删除表中的数据:

DELETE FROM xiang.test;

从文本文件中恢复数据:

LOAD DATA INFILE '/var/lib/mysql-files/test.txt' INTO TABLE xiang.test;

举例2:

以固定格式导入数据文件

LOAD DATA INFILE '/var/lib/mysql-files/test.txt' INTO TABLE xiang.test FIELDS TERMINATED BY ',' ENCLOSED BY '\"';

7.6.2.2 使用mysqlimport

举例:

导出文件test.txt,字段之间使用逗号","间隔,字段值用双引号括起来:

SELECT * FROM xiang.test INTO OUTFILE '/var/lib/mysql-files/test.txt' FIELDSTERMINATED BY ',' ENCLOSED BY '\"';

删除表中的数据:

DELETE FROM xiang.test;

从文本文件中恢复数据:

mysqlimport -uroot -p xiang '/var/lib/mysql-files/test.txt' --fields-terminatedby=',' --fields-optionally-enclosed-by='\"

7.7 数据库迁移

7.7.1 概述

数据迁移(data migration)是指选择、准备、提取和转换数据,并将数据从一个计算机存储系统永久地传输到另一个计算机存储系统的过程。

此外, 验证迁移数据的完整性和退役原来旧的数据存储,也被认为是整个数据迁移过程的一部分。

数据库迁移的原因是多样的,包括服务器或存储设备更换、维护或升级,应用程序迁移,网站集成,灾难恢复和数据中心迁移。

根据不同的需求可能要采取不同的迁移方案,但总体来讲,MySQL 数据迁移方案大致可以分为物理迁移和逻辑迁移两类。通常以尽可能自动化的方式执行,从而将人力资源从繁琐的任务中解放出来。

7.7.2 迁移方案

物理迁移

物理迁移适用于大数据量下的整体迁移。使用物理迁移方案的优点是比较快速,但需要停机迁移并且要求 MySQL 版本及配置必须和原服务器相同,也可能引起未知问题。物理迁移包括拷贝数据文件和使用 XtraBackup 备份工具两种。不同服务器之间可以采用物理迁移,我们可以在新的服务器上安装好同版本的数据库软件,创建好相同目录,建议配置文件也要和原数据库相同,然后从原数据库方拷贝来数据文件及日志文件,配置好文件组权限,之后在新服务器这边使用mysqld命令启动数据库。

逻辑迁移

逻辑迁移适用范围更广,无论是部分迁移还是全量迁移 ,都可以使用逻辑迁移。逻辑迁移中使用最多的就是通过mysqldump等备份工具。

7.7.3 迁移注意点

1. 相同版本的数据库之间迁移注意点

指的是在主版本号相同的MySQL数据库之间进行数据库移动。

方式1: 因为迁移前后MySQL数据库的主版本号相同 ,所以可以通过复制数据库目录来实现数据库迁移,但是物理迁移方式只适用于MyISAM引擎的表。对于InnoDB表,不能用直接复制文件的方式备份数据库。

方式2: 最常见和最安全的方式是使用 mysqldump命令 导出数据,然后在目标数据库服务器中使用MySQL命令导入。

举例:

#host1的机器中备份所有数据库,并将数据库迁移到名为host2的机器上 mysqldump –h host1 –uroot –p –-all-databases | mysql –h host2 –uroot –p

在上述语句中,“|”符号表示管道,其作用是将mysqldump备份的文件给mysql命令;“--all-databases”表示要迁移所有的数据库。通过这种方式可以直接实现迁移。

2. 不同版本的数据库之间迁移注意点

例如,原来很多服务器使用5.7版本的MySQL数据库,在8.0版本推出来以后,改进了5.7版本的很多缺陷,因此需要把数据库升级到8.0版本.

旧版本与新版本的MySQL可能使用不同的默认字符集,例如有的旧版本中使用latin1作为默认字符集,而最新版本的MySQL默认字符集为utf8mb4。

如果数据库中有中文数据,那么迁移过程中需要对默认字符集进行修改 ,不然可能无法正常显示数据。

高版本的MySQL数据库通常都会兼容低版本 ,因此可以从低版本的MySQL数据库迁移到高版本的MySQL数据库。

3. 不同数据库之间迁移注意点

不同数据库之间迁移是指从其他类型的数据库迁移到MySQL数据库,或者从MySQL数据库迁移到其他类型的数据库。这种迁移没有普适的解决方法。

迁移之前,需要了解不同数据库的架构, 比较它们之间的差异。

不同数据库中定义相同类型的数据的关键字可能会不同 。例如,

  • MySQL中日期字段分为DATE和TIME两种,而ORACLE日期字段只有DATE;
  • SQLServer数据库中有ntext、Image等数据类型,MySQL数据库没有这些数据类型;
  • MySQL支持的ENUM和SET类型,这些SQL Server数据库不支持。

另外,数据库厂商并没有完全按照SQL标准来设计数据库系统,导致不同的数据库系统的SQL语句有差别。例如,微软的SQL Server软件使用的是T-SQL语句,T-SQL中包含了非标准的SQL语句,不能和MySQL的SQL语句兼容。

不同类型数据库之间的差异造成了互相迁移的困难 ,这些差异其实是商业公司故意造成的技术壁垒。但是不同类型的数据库之间的迁移并不是完全不可能 。例如,可以使用 MyODBC实现MySQL和SQL Server之间的迁移。MySQL官方提供的工具 MySQL Migration Toolkit也可以在不同数据之间进行数据迁移。MySQL迁移到Oracle时,需要使用mysqldump命令导出sql文件,然后, 手动更改sql文件中的CREATE语句。

7.7.4 迁移小结

7.8 误删库后如何处理

7.8.1 delete:误删行

经验之谈:

  1. 恢复数据比较安全的做法,是恢复出一个备份 ,或者找一个从库作为临时库 ,在这个临时库上执行这些操作,然后再将确认过的临时库的数据,恢复回主库。如果直接修改主库,可能导致对数据的二次破坏。
  2. 当然,针对预防误删数据的问题,建议如下:
    1. 把sql_safe_updates参数设置为on 。这样一来,如果我们忘记在delete或者update语句中写where条件,或者where条件里面没有包含索引字段的话,这条语句的执行就会报错。如果确定要把一个小表的数据全部删掉,在设置了sql_safe_updates=on情况下,可以在delete语句中加上where条件,比如where id>=0。
    2. 代码上线前,必须经过 SQL审计 。

7.8.2 truncate/drop:误删库\表

方案:

这种情况下,要想恢复数据,就需要使用全量备份 ,加增量日志 的方式了。这个方案要求线上有定期的全量备份,并且实时备份binlog。

在这两个条件都具备的情况下,假如有人中午12点误删了一个库,恢复数据的流程如下:

  1. 取最近一次全量备份 ,假设这个库是一天一备,上次备份是当天凌晨2点 ;
  2. 用备份恢复出一个 临时库 ;
  3. 从日志备份里面,取出凌晨2点之后的日志;
  4. 把这些日志,除了误删除数据的语句外,全部应用到临时库。

7.8.3 延迟复制备库

如果有非常核心的业务,不允许太长的恢复时间,可以考虑搭建延迟复制的备库。一般的主备复制结构存在的问题是,如果主库上有个表被误删了,这个命令很快也会被发给所有从库,进而导致所有从库的数据表也都一起被误删了。

延迟复制的备库是一种特殊的备库,通过 CHANGE MASTER TO MASTER_DELAY = N 命令,可以指定这个备库持续保持跟主库有N秒的延迟 。比如你把N设置为3600,这就代表了如果主库上有数据被误删了,并且在1小时内发现了这个误操作命令,这个命令就还没有在这个延迟复制的备库执行。这时候到这个备库上执行stop slave,再通过之前介绍的方法,跳过误操作命令,就可以恢复出需要的数据。

7.8.4 预防误删库表的方法

  1. 账号分离 。这样做的目的是,避免写错命令。比如:
    1. 只给业务开发同学DML权限,而不给truncate/drop权限。而如果业务开发人员有DDL需求的话,可以通过开发管理系统得到支持。
    2. 即使是DBA团队成员,日常也都规定只使用只读账号 ,必要的时候才使用有更新权限的账号。
  2. 制定操作规范 。比如:
    1. 在删除数据表之前,必须先对表做改名操作。然后,观察一段时间,确保对业务无影响以后再删除这张表。
    2. 改表名的时候,要求给表名加固定的后缀(比如加 _to_be_deleted ),然后删除表的动作必须通过管理系统执行。并且,管理系统删除表的时候,只能删除固定后缀的表。

7.8.5 rm:误删mysql实例

对于一个有高可用机制的MySQL集群来说,不用担心rm删除数据 了。只是删掉了其中某一个节点的数据的话,HA系统就会开始工作,选出一个新的主库,从而保证整个集群的正常工作。我们要做的就是在这个节点上把数据恢复回来,再接入整个集群。

7.9 Mysql常用命令

7.9.1 mysql

该mysql不是指mysql服务,而是指mysql的客户端工具。语法 :

1. 连接选项

#参数 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器IP或域名
-P, --port=# 指定连接端口
#示例 :
mysql -h 127.0.0.1 -P 3306 -u root -p
mysql -h127.0.0.1 -P3306 -uroot -p密码

2. 执行选项

-e, --execute=name 执行SQL语句并退出

此选项可以在Mysql客户端执行SQL语句,而不用连接到MySQL数据库再执行,对于一些批处理脚本,这种方式尤其方便。

mysql -uroot -p db01 -e "select * from tb_book";

7.9.2 mysqladmin

mysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。

可以通过 : mysqladmin --help 指令查看帮助文档

#示例 :
mysqladmin -uroot -p create 'test01';
mysqladmin -uroot -p drop 'test01';
mysqladmin -uroot -p version;

7.9.3 mysqlbinlog

由于服务器生成的二进制日志文件以二进制格式保存,所以如果想要检查这些文本的文本格式,就会使用到mysqlbinlog日志管理工具。

语法 :

mysqlbinlog [options] log-files1 log-files2 ...
#选项:
-d, --database=name : 指定数据库名称,只列出指定的数据库相关操作。
-o, --offset=# : 忽略掉日志中的前n行命令。
-r,--result-file=name : 将输出的文本格式日志输出到指定文件。
-s, --short-form : 显示简单格式, 省略掉一些信息。
--start-datatime=date1 --stop-datetime=date2 : 指定日期间隔内的所有日志。
--start-position=pos1 --stop-position=pos2 : 指定位置间隔内的所有日志。

7.9.4 mysqldump

mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表,及插入表的SQL语句。语法 :

mysqldump [options] db_name [tables]
mysqldump [options] --database/-B db1 [db2 db3...]
mysqldump [options] --all-databases/-A

1.连接选项

#参数 :
-u, --user=name 指定用户名
-p, --password[=name] 指定密码
-h, --host=name 指定服务器IP或域名
-P, --port=# 指定连接端口

2.输出内容选项

#参数:
--add-drop-database 在每个数据库创建语句前加上 Drop database 语句
--add-drop-table 在每个表创建语句前加上 Drop table 语句 , 默认开启 ; 不开启 (--skip-add-drop-table)
-n, --no-create-db 不包含数据库的创建语句
-t, --no-create-info 不包含数据表的创建语句
-d --no-data 不包含数据
-T, --tab=name 自动生成两个文件:一个.sql文件,创建表结构的语句;一个.txt文件,数据文件,相当于select into outfile

#示例 :

mysqldump -uroot -p db01 tb_book --add-drop-database --add-drop-table > a
mysqldump -uroot -p -T /tmp test city

7.9.5 mysqlimport/source

mysqlimport 是客户端数据导入工具,用来导入mysqldump 加 -T 参数后导出的文本文件。语法:

mysqlimport [options] db_name textfile1 [textfile2...]

示例:

mysqlimport -uroot -p test /tmp/city.txt

如果需要导入sql文件,可以使用mysql中的source 指令 :

source /root/tb_book.sql

7.9.6 mysqlshow

mysqlshow 客户端对象查找工具,用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。语法:

mysqlshow [options] [db_name [table_name [col_name]]]

参数:

--count 显示数据库及表的统计信息(数据库,表 均可以不指定)
-i 显示指定数据库或者指定表的状态信息

示例:

#查询每个数据库的表的数量及表中记录的数量
mysqlshow -uroot -p --count
mbp:~ xiang$ mysqlshow -uroot -p --count
Enter password: 
+--------------------+--------+--------------+
|     Databases      | Tables |  Total Rows  |
+--------------------+--------+--------------+
| mysql              | N/A    | N/A          |
| omc_rm             | N/A    | N/A          |
| performance_schema | N/A    | N/A          |
| REDIRECT           | N/A    | N/A          |
| sys                | N/A    | N/A          |
| test               | N/A    | N/A          |
| xiang              | N/A    | N/A          |
+--------------------+--------+--------------+

#查询test库中每个表中的字段书,及行数
mysqlshow -uroot -p xiang --count
mbp:~ xiang$ mysqlshow -uroot -p xiang  --count
Enter password: 
Database: xiang
+-------------+----------+------------+
|   Tables    | Columns  | Total Rows |
+-------------+----------+------------+
| emp1        |        4 |          2 |
| employee    |        5 |          0 |
| employee2   |        3 |          0 |
| event       |        3 |          3 |
| score       |        2 |          1 |
| test        |        3 |          1 |
| xiangMyISAM |        1 |          0 |
+-------------+----------+------------+
7 rows in set.

# 查询xiang库中test表的详细情况
mbp:~ xiang$ mysqlshow -uroot -p xiang test --count
Enter password: 
Database: xiang  Table: test  Rows: 1
+-------+------+-----------+------+-----+---------+-------------------+---------------------------------+---------+
| Field | Type | Collation | Null | Key | Default | Extra             | Privileges                      | Comment |
+-------+------+-----------+------+-----+---------+-------------------+---------------------------------+---------+
| a     | int  |           | YES  |     |         |                   | select,insert,update,references |         |
| b     | int  |           | YES  |     |         |                   | select,insert,update,references |         |
| c     | int  |           | YES  |     |         | VIRTUAL GENERATED | select,insert,update,references |         |
+-------+------+-----------+------+-----+---------+-------------------+---------------------------------+---------+

八、结束

  • 容易走的路,都是下坡路。当你觉得选择的路很艰难,很累,很难受的时候。说明你可能在成长,你在走上坡路。
  • 当你觉得选择的路很容易,很爽,很舒服的时候。说明你可能在逃避,你在走下坡路。
  • 顶尖高手,比的是慢、是笨、是扎实,是聪明人下笨功夫。
  • 时间是什么?时间是用来打造你的核心竞争壁垒的工具!

おすすめ

転載: blog.csdn.net/guaituo0129/article/details/130675141