はじめに
トランザクションは、データベースが関連するステートメントをグループ化し、システムによって単一のユニットとして実行できるようにする方法です。ステートメントはユニットとして扱われ、すべての構成要素が正常に完了するか、元の状態に戻るかのいずれかになります。これは、単一の目標を達成するために複数の個別のステップを必要とする変更の一貫性を維持する1つの方法です。
この記事では、トランザクションとは何か、そしてトランザクションがどのように役立つかについて説明します。次に、MySQLがトランザクションをさまざまな方法で使用して、ステートメントがデータベースにどのように適用されるかを正確に制御する方法を見ていきます。
トランザクションとは?
トランザクションは、複数のステートメントをグループ化および分離して、単一の操作として処理する方法です。トランザクションでは、各コマンドをサーバーに送信された順に個別に実行するのではなく、コマンドをまとめてバンドルし、他のリクエストとは別のコンテキストで実行します。
分離は、トランザクションの重要な部分です。トランザクション内では、実行されたステートメントはトランザクション自体の環境にのみ影響を与えることができます。トランザクション内からステートメントはデータを変更でき、その結果はすぐに表示されます。外部からは、トランザクションがコミットされるまで変更は行われず、コミットされると、トランザクション内のすべてのアクションが一度に表示されるようになります。
これらの機能は、ACIDコンプライアンスを達成するのに役立ちます。原子性(トランザクション内のアクションはすべてコミットされるか、すべてロールバックされるかのいずれか)と分離(トランザクションの外部では、コミットまで何も変更されませんが、内部では、各ステートメントは即座に影響を与えます)を提供することによって実現します。これらは共に、データベースが一貫性を維持するのに役立ちます(部分的なデータ変換が発生しないことを保証することによって)。さらに、トランザクションの変更は、不揮発性ストレージにコミットされるまで成功として返されず、永続性を提供します。
これらの目標を達成するために、トランザクションは多くの異なる戦略を採用しており、異なるデータベースシステムは異なる方法を使用しています。MySQLは多版同時実行制御(MVCC)と呼ばれるシステムを使用しており、これによりデータベースはデータスナップショットを使用してこれらのアクションを実行できます。全体として、これらのシステムは最新のリレーショナルデータベースの基本的な構成要素の1つを構成し、クラッシュに強い方法で複雑なデータを安全に処理できるようにします。
整合性障害の種類
人々がトランザクションを使用する理由の1つは、データの一貫性とそれが処理される環境について特定の保証を得るためです。整合性はさまざまな方法で損なわれる可能性があり、データベースがそれらをどのように防止しようとするかに影響を与えます。
トランザクションの実装に応じて、整合性が損なわれる可能性のある主な方法が4つあります。これらのシナリオが発生する可能性のあるシナリオに対する許容度は、アプリケーションでトランザクションをどのように使用するかに影響します。
ダーティリード
ダーティリードは、トランザクション内のステートメントが、他の進行中のトランザクションによって書き込まれたデータを読み取ることができる場合に発生します。これは、トランザクションのステートメントがまだコミットされていない場合でも、それらを読み取ることができ、他のトランザクションに影響を与える可能性があることを意味します。
これは多くの場合、整合性の重大な侵害と見なされます。トランザクションが互いに適切に分離されていないためです。データベースにコミットされない可能性のあるステートメントが、他のトランザクションの実行に影響を与え、その動作を変更する可能性があります。
ダーティリードを許可するトランザクションは、結果のデータの一貫性について合理的な主張をすることはできません。
非再現リード
非再現リードは、トランザクションの外部でのコミットがトランザクション内で見られるデータを変更した場合に発生します。トランザクション内で、同じデータが2回読み取られたが、各インスタンスで異なる値が取得された場合、このタイプの問題を認識できます。
ダーティリードと同様に、非再現リードを許可するトランザクションは、トランザクション間の完全な分離を提供しません。違いは、非再現リードでは、トランザクションに影響を与えるステートメントが実際にはトランザクションの外部でコミットされていることです。
ファントムリード
ファントムリードは、トランザクション内でクエリが2回実行されたときに返される行が異なる場合に発生する、非再現リードの特定のタイプです。
たとえば、トランザクション内のクエリが最初に実行されたときに4行を返し、2回目に5行を返す場合、これはファントムリードです。ファントムリードは、クエリを満たす行数を変更するトランザクションの外部でのコミットによって引き起こされます。
シリアライゼーション異常
シリアライゼーション異常は、同時にコミットされた複数のトランザクションの結果が、それらが次々とコミットされた場合とは異なる結果になる場合に発生します。これは、トランザクションが競合を解決せずに同じテーブルまたはデータを変更する2つのコミットが発生することを許可するたびに発生する可能性があります。
トランザクション分離レベル
トランザクションは、「万能」なソリューションではありません。さまざまなシナリオで、パフォーマンスと保護の間に異なるトレードオフが必要です。幸いなことに、MySQLでは、必要なトランザクション分離のタイプを指定できます。
ほとんどのデータベースシステムで提供される分離レベルには、次のものがあります。
リードアンコミット
リードアンコミットは、データの一貫性と分離の維持について最も少ない保証を提供する分離レベルです。read uncommitted
を使用するトランザクションには、複数のステートメントを一度にコミットしたり、間違いが発生した場合にステートメントをロールバックしたりする機能など、トランザクションによく関連付けられる特定の機能がありますが、整合性が損なわれる可能性のある状況を数多く許容します。
read uncommitted
分離レベルで構成されたトランザクションでは、以下が許可されます。
- ダーティリード
- 非再現リード
- ファントムリード
- シリアライゼーション異常
このレベルの分離は、データの一貫性と分離についてほとんど保証を提供しないため、一般的には推奨されません。これは主に、「全か無か」のコミットメントモデルでステートメントをグループ化する必要があるが、整合性の保証が必要ない場合に役立ちます(非常にまれなケース)。
リードコミット
リードコミットは、ダーティリードから具体的に保護する分離レベルです。トランザクションがread committed
レベルの一貫性を使用する場合、コミットされていないデータはトランザクションの内部コンテキストに影響を与えることはありません。これにより、コミットされていないデータがトランザクションに影響を与えないようにすることで、基本的なレベルの一貫性が提供されます。
read committed
はread uncommitted
よりも優れた保護を提供しますが、すべてのタイプの一貫性のない状態から保護するわけではありません。これらの問題はまだ発生する可能性があります。
- 非再現リード
- ファントムリード
- シリアライゼーション異常
再現可能リード
再現可能リード分離レベルは、read committed
によって提供される保証に基づいて構築されています。以前と同様にダーティリードを回避しますが、非再現リードも防止します。
repeatable read
は非再現リードとダーティリードを防止できますが、次の分離問題がまだ発生する可能性があります。
- ファントムリード
- シリアライゼーション異常
ほとんどの場合、ファントムリードも防止されますが、依然として発生する可能性のある状況がいくつかあります。リンクされた例では、トランザクションのSELECT
クエリは結果を返しません。別のトランザクションによって行が挿入およびコミットされた後でも同様です。ただし、新しい行を更新するクエリを発行すると、更新は他のトランザクションによってコミットされた行を認識しないはずであるにもかかわらず、クエリはデータを返します。その後、SELECT
クエリはデータを返します。
MySQLのInnoDBエンジン(ほとんどの場合の通常のエンジン)の場合、再現可能リード分離メソッドがデフォルトです。
シリアライザブル
シリアライザブル分離レベルは、最高レベルの分離と一貫性を提供します。これは、repeatable read
レベルが行うすべてのシナリオを防止し、シリアライゼーション異常の可能性も排除します。
シリアライザブル分離は、同時トランザクションが次々と実行されたかのようにコミットされることを保証します。シリアライゼーション異常が発生する可能性のあるシナリオが発生した場合、データセットに不整合を導入する代わりに、トランザクションの1つにシリアライゼーションエラーが発生します。
トランザクションの定義
MySQLがトランザクションで使用できるさまざまな分離レベルについて説明したので、トランザクションを定義する方法を説明しましょう。
MySQLでは、デフォルトでは、明示的にマークされたトランザクション外のすべてのステートメントは、実際には独自の単一ステートメントトランザクションで実行されます。トランザクションブロックを明示的に開始するには、START TRANSACTION
またはBEGIN
コマンドを使用できます。START TRANSACTION
形式はBEGIN
形式では使用できない修飾子を使用できるため、MySQLではSTART TRANSACTION
を使用することをお勧めします。トランザクションをコミットするには、COMMIT
コマンドを発行します。
したがって、トランザクションの基本的な構文は次のようになります。
START TRANSACTION;statementsCOMMIT;
より具体的な例として、1つのアカウントから別のアカウントに1000ドルを転送しようとしていると想像してください。お金は常に2つのアカウントのいずれかにあり、両方にあることはないようにする必要があります。
この転送をカプセル化する2つのステートメントを、次のようなトランザクションでラップできます。
START TRANSACTION;UPDATE accountsSET balance = balance - 1000WHERE id = 1;UPDATE accountsSET balance = balance + 1000WHERE id = 2;COMMIT;
ここでは、id = 2
のアカウントに1000ドルを入金せずに、id = 1
のアカウントから1000ドルが引き落とされることはありません。これらの2つのステートメントはトランザクション内で順番に実行されますが、コミットされるため、基になるデータセットで同時に実行されます。
トランザクションのロールバック
トランザクション内では、ステートメントのすべてまたはnoneがデータベースにコミットされます。トランザクション内で行われたステートメントと変更をデータベースに適用する代わりに破棄することは、トランザクションの「ロールバック」として知られています。
トランザクションは、自動または手動でロールバックできます。MySQLは、問題を防ぐために、トランザクション内のステートメントの1つがエラーになった場合や、その他のシナリオでトランザクションを自動的にロールバックします。
現在のトランザクション中に与えられたステートメントを手動でロールバックするには、ROLLBACK
コマンドを使用できます。これにより、トランザクション内のすべてのステートメントがキャンセルされ、事実上、トランザクションの開始時に時間が戻ります。
たとえば、以前使用していた銀行口座の例を使用していると仮定して、UPDATE
ステートメントを発行した後、誤って間違った金額を転送したか、間違ったアカウントを使用したことが判明した場合、変更をコミットする代わりにロールバックできます。
START TRANSACTION;UPDATE accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE id = 3; -- Wrong account number here! Must rollback/* Gets us back to where we were before the transaction started */ROLLBACK;
ROLLBACK
すると、1500ドルはid = 1
のアカウントに残ります。
ロールバック時のセーブポイントの使用
デフォルトでは、ROLLBACK
コマンドは、START TRANSACTION
またはBEGIN
コマンドが最初に呼び出された時点にトランザクションをリセットします。しかし、トランザクション内の一部のステートメントのみを元に戻したい場合はどうすればよいでしょうか?
ROLLBACK
コマンドを発行するときにロールバックする任意の位置を指定することはできませんが、トランザクション全体で設定した「セーブポイント」にロールバックできます。SAVEPOINT
コマンドでトランザクションの場所を事前にマークし、ロールバックする必要がある場合にそれらの特定の場所を参照できます。
これらのセーブポイントを使用すると、中間ロールバックポイントを作成できます。その後、現在位置からセーブポイントまでの間に行われたステートメントを必要に応じて元に戻し、トランザクションの作業を続行できます。
セーブポイントを指定するには、SAVEPOINT
コマンドの後にセーブポイントの名前を発行します。
SAVEPOINT save_1;
そのセーブポイントにロールバックするには、ROLLBACK TO
コマンドを使用します。
ROLLBACK TO save_1;
これまで使用してきたアカウント中心の例を続けましょう。
START TRANSACTION;UPDATE accountsSET balance = balance - 1500WHERE id = 1;/* Set a save point that we can return to */SAVEPOINT save_1;UPDATE accountsSET balance = balance + 1500WHERE id = 3; -- Wrong account number here! We can rollback to the save point though!/* Gets us back to the state of the transaction at `save_1` */ROLLBACK TO save_1;/* Continue the transaction with the correct account number */UPDATE accountsSET balance = balance + 1500WHERE id = 4;COMMIT;
ここでは、トランザクションでこれまで行ってきたすべての作業を失うことなく、犯した間違いから回復できます。ロールバック後、正しいステートメントを使用して計画どおりにトランザクションを続行します。
トランザクションの分離レベルの設定
トランザクションに必要な分離レベルを設定するには、ISOLATION LEVEL
句とともにSET TRANSACTION
ステートメントを使用できます。SET TRANSACTION
ステートメントを使用すると、トランザクションの分離レベルと読み取りおよび書き込み権限を変更できます。
デフォルトでは、SET TRANSACTION
ステートメントは、開始される次のトランザクションのプロパティにのみ影響します。これは、START TRANSACTION
またはBEGIN
ステートメントの前に指定する必要があります。基本的な構文は次のようになります。
SET TRANSACTION ISOLATION LEVEL <isolation_level>;START TRANSACTION;statementsCOMMIT;
<isolation_level>
は、次のいずれかになります(詳細は前述)。
リードアンコミット
リードコミット
再現可能リード
(MySQLのデフォルトの動作モード)シリアライザブル
コマンドを発行するときに、GLOBAL
またはSESSION
キーワードを指定して、別のスコープに影響を与えることもできます。
SET GLOBAL TRANSACTION ISOLATION LEVEL
と入力すると、分離レベルは今後のすべてのセッションに対してグローバルに変更されます。現在のセッションは古い分離レベルを使用するため、トランザクションレベルまたはSESSION
レベルを使用してそれらを変更する必要がある場合は、これらのスコープを明示的に変更してください。
SESSION
修飾子(SET SESSION TRANSACTION ISOLATION LEVEL
など)を使用すると、同じセッション内の今後のすべてのトランザクションの分離レベルを変更できます。繰り返しますが、これは既存のトランザクションには影響しません。
SET TRANSACTION
で変更できるもう1つの側面は、トランザクションが読み取り/書き込み可能か読み取り専用かです。デフォルトでは、MySQLのトランザクションは読み取りおよび書き込み可能です。SET TRANSACTION READ ONLY
を使用すると、セッションを読み取り専用にすることができます。セッションを明示的に読み取り/書き込み可能にする場合は、SET TRANSACTION READ WRITE
を発行することもできます。
トランザクションのチェイニング
順番に実行する必要がある複数のトランザクションがある場合は、COMMIT AND CHAIN
コマンドを使用してそれらをチェーンすることができます。
COMMIT AND CHAIN
コマンドは、トランザクション内のステートメントをコミットすることにより、現在のトランザクションを完了します。コミットが処理された後、同じ分離レベルで新しいトランザクションをすぐに開きます。これにより、別のステートメントのセットをトランザクションにグループ化できます。
このステートメントは、COMMIT; SET TRANSACTION ISOLATION LEVEL <isolation_level>; START TRANSACTION
を発行した場合とまったく同じように機能します。
SET TRANSACTION ISOLATION LEVEL READ COMMITTEDSTART TRANSACTION;UPDATE accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE id = 2;/* Commit the data and start a new transaction that will take into account the committed data and isolation level from the last transaction */COMMIT AND CHAIN;UPDATE accountsSET balance = balance - 1000WHERE id = 2;UPDATE accountsSET balance = balance + 1000WHERE id = 3;COMMIT;
トランザクションのチェイニングは、トランザクションレベルを毎回明示的に設定したり、セッションまたはグローバルデフォルトを変更したりすることなく、複数のトランザクションを作成するのに役立ちます。
結論
トランザクションは、データを一貫性のある状態に保つのに役立つ便利な機能を提供します。ただし、さまざまな分離レベルには、留意しておくべき多くのトレードオフがあります。ユースケースに適したレベルの保護を決定するには、ある程度の調査と検討が必要になる場合があります。これは、トランザクションが長時間実行される場合に特に当てはまります。コミットが発生する前にデータベースが大幅に変更される可能性があるため、ロールバックやより多くの手作業につながる可能性があります。
欠点はあるものの、トランザクションは、リレーショナルデータベースが提供することが期待されているACID保証に大きく貢献しているため、多くのシナリオで依然として役立ちます。トランザクションをいつ使用するか、どのようなタイプの分離レベルが適切か、および自動ロールバックを回避する方法を理解することは、投資する価値のある知識です。