はじめに
トランザクションは、データベースが関連するステートメントをシステムによって単一の単位として実行されるようにグループ化する方法です。ステートメントは単一の単位として処理され、すべての構成要素が正常に完了するか、元の状態に戻るかのいずれかになります。これは、単一の目標を達成するために複数の個別のステップを要する変更の整合性を維持する一つの方法です。
この記事では、トランザクションとは何か、そしてそれがどのように役立つかについて説明します。次に、MySQLがさまざまな方法でトランザクションを使用して、ステートメントがデータベースにどのように適用されるかを正確に制御する方法を見ていきます。
トランザクションとは?
トランザクションは、複数のステートメントを単一の操作として処理するためにグループ化し、分離する方法です。各コマンドがサーバーに送信されるたびに個別に実行されるのではなく、トランザクションではコマンドがまとめてバンドルされ、他のリクエストとは別のコンテキストで実行されます。
分離性は、トランザクションの重要な要素です。トランザクション内では、実行されたステートメントはトランザクション自体の環境にのみ影響します。トランザクションの内部からは、ステートメントはデータを変更でき、その結果はすぐに表示されます。外部からは、トランザクションがコミットされるまで変更は行われず、コミットされた時点でトランザクション内のすべてのアクションが一度に表示されるようになります。
これらの機能は、原子性(トランザクション内のアクションはすべてコミットされるか、すべてロールバックされるか)、および分離性(トランザクションの外部ではコミットされるまで何も変更されず、内部では各ステートメントが即座に結果をもたらす)を提供することにより、データベースがACID準拠を達成するのに役立ちます。これらを合わせることで、データベースが整合性を維持するのに役立ちます(部分的なデータ変換が発生しないことを保証することで)。さらに、トランザクション内の変更は、不揮発性ストレージにコミットされるまで成功として返されず、これにより永続性が提供されます。
これらの目標を達成するために、トランザクションはさまざまな戦略を採用し、異なるデータベースシステムは異なる方法を使用します。MySQLは、データスナップショットを使用してデータベースがこれらのアクションを実行できるようにする多版型同時実行制御 (MVCC)と呼ばれるシステムを使用します。これらすべてのシステムは、現代のリレーショナルデータベースの基本的な構成要素の一つであり、クラッシュ耐性のある方法で複雑なデータを安全に処理できるようにします。
整合性障害の種類
人々がトランザクションを使用する理由の一つは、データの整合性とそれが処理される環境に関する特定の保証を得るためです。整合性はさまざまな方法で破られる可能性があり、それがデータベースがそれらを防ぐ方法に影響を与えます。
トランザクションの実装に応じて、整合性の欠如が発生する主な方法が4つあります。これらのシナリオが発生する可能性のある状況に対する許容度が、アプリケーションでのトランザクションの使用方法に影響を与えます。
ダーティリード
ダーティリードは、トランザクション内のステートメントが他の進行中のトランザクションによって書き込まれたデータを読み取ることができる場合に発生します。これは、トランザクションのステートメントがまだコミットされていなくても、それらが読み取られ、したがって他のトランザクションに影響を与える可能性があることを意味します。
これは、トランザクションが互いに適切に分離されていないため、整合性の重大な違反と見なされることがよくあります。データベースにコミットされない可能性のあるステートメントが、他のトランザクションの実行に影響を与え、その動作を変更する可能性があります。
ダーティリードを許可するトランザクションは、結果のデータの整合性について合理的な主張をすることはできません。
ノンリピータブルリード
ノンリピータブルリードは、トランザクション外部でのコミットがトランザクション内で参照されるデータを変更する場合に発生します。この種の問題は、トランザクション内で同じデータが2回読み取られ、各インスタンスで異なる値が取得される場合に認識できます。
ダーティリードと同様に、ノンリピータブルリードを許可するトランザクションは、トランザクション間の完全な分離性を提供しません。違いは、ノンリピータブルリードでは、トランザクションに影響を与えるステートメントが実際にトランザクションの外部でコミットされていることです。
ファントムリード
ファントムリードは、トランザクション内でクエリが2回目に実行されたときに返される行が異なる場合に発生する、ノンリピータブルリードの特定のタイプです。
たとえば、トランザクション内のクエリが最初に実行されたときに4行を返し、2回目に5行を返す場合、これはファントムリードです。ファントムリードは、クエリを満たす行の数を変更するトランザクション外部でのコミットによって引き起こされます。
直列化異常
直列化異常は、複数のトランザクションが同時にコミットされた結果が、それらが一つずつコミットされた場合とは異なる結果になる場合に発生します。これは、トランザクションが競合を解決せずに、同じテーブルまたはデータをそれぞれ変更する2つのコミットを許可するたびに発生する可能性があります。
トランザクション分離レベル
トランザクションは「万能」なソリューションではありません。シナリオによって、パフォーマンスと保護の間で異なるトレードオフが求められます。幸いなことに、MySQLでは必要なトランザクション分離のタイプを指定できます。
ほとんどのデータベースシステムで提供される分離レベルは次のとおりです。
リードアンコミッテッド
リードアンコミッテッドは、データの整合性と分離性の維持に関して最も保証が少ない分離レベルです。read uncommitted
を使用するトランザクションには、一度に複数のステートメントをコミットする機能や、間違いが発生した場合にステートメントをロールバックする機能など、トランザクションに頻繁に関連付けられる特定の機能がありますが、整合性が損なわれる可能性のある多くの状況を許可します。
read uncommitted
分離レベルで設定されたトランザクションは、以下を許可します。
- ダーティリード
- ノンリピータブルリード
- ファントムリード
- 直列化異常
この分離レベルは、データの整合性と分離性に関してほとんど保証を提供しないため、一般的には推奨されません。「すべてか無か」のコミットメントモデルでステートメントをグループ化する必要があるが、整合性の保証が不要な場合(非常にまれなケース)に主に役立ちます。
リードコミッテッド
リードコミッテッドは、特にダーティリードから保護する分離レベルです。トランザクションがread committed
整合性レベルを使用する場合、未コミットのデータはトランザクションの内部コンテキストに決して影響を与えません。これにより、未コミットのデータがトランザクションに影響を与えることがないようにすることで、基本的な整合性レベルが提供されます。
Although read committed
はread uncommitted
よりも優れた保護を提供しますが、すべての種類の整合性の欠如から保護するわけではありません。これらの問題はまだ発生する可能性があります。
- ノンリピータブルリード
- ファントムリード
- 直列化異常
リピータブルリード
リピータブルリード分離レベルは、read committed
によって提供される保証を基盤としています。以前と同様にダーティリードを回避しますが、ノンリピータブルリードも防ぎます。
repeatable read
はノンリピータブルリードとダーティリードを防ぐことができますが、これらの分離問題には依然として苦しむ可能性があります。
- ファントムリード
- 直列化異常
ほとんどの場合、ファントムリードも防がれますが、まだ発生する可能性のある状況がいくつかあります。リンクされた例では、トランザクション内のSELECT
クエリは、別のトランザクションによって行が挿入およびコミットされた後でも、結果を返しません。しかし、新しい行を更新するクエリを発行すると、そのクエリはデータを返します。これは、更新が別のトランザクションによってコミットされた行について知らないはずであるにもかかわらずです。その後、SELECT
クエリはデータを返します。
MySQLのInnoDBエンジン(ほとんどの場合の標準エンジン)では、リピータブルリード分離方式がデフォルトです。
シリアライザブル
シリアライザブル分離レベルは、最高の分離性と整合性を提供します。repeatable read
レベルが防ぐすべてのシナリオを防ぎつつ、直列化異常の可能性も排除します。
シリアライザブル分離は、同時実行されるトランザクションが、あたかも一つずつ実行されたかのようにコミットされることを保証します。直列化異常が発生する可能性のあるシナリオが発生した場合、データセットに整合性の欠如を導入する代わりに、いずれかのトランザクションが直列化失敗となります。
トランザクションの定義
MySQLがトランザクションで使用できるさまざまな分離レベルについて説明したところで、トランザクションの定義方法をデモンストレーションしましょう。
MySQLでは、デフォルトで、明示的にマークされたトランザクションの外部にあるすべてのステートメントは、実際には独自の単一ステートメントトランザクションとして実行されます。トランザクションブロックを明示的に開始するには、START TRANSACTION
またはBEGIN
コマンドを使用できます。START TRANSACTION
形式はBEGIN
形式では使用できない修飾子を受け入れるため、MySQLはSTART TRANSACTION
の使用を推奨しています。トランザクションをコミットするには、COMMIT
コマンドを発行します。
したがって、トランザクションの基本構文は次のようになります。
START TRANSACTION;statementsCOMMIT;
より具体的な例として、1000ドルをある口座から別の口座へ送金しようとしていると想像してみてください。このお金が常に2つの口座のいずれか一方にあり、両方には決してないことを確認したいとします。
この送金をカプセル化する2つのステートメントを、次のようなトランザクションで囲むことができます。
START TRANSACTION;UPDATE accountsSET balance = balance - 1000WHERE id = 1;UPDATE accountsSET balance = balance + 1000WHERE id = 2;COMMIT;
ここでは、id = 1
の口座から1000ドルが引き出されることは、同時にid = 2
の口座に1000ドルが入金されることなしにはありません。これらの2つのステートメントはトランザクションの内部で順次実行されますが、同時にコミットされ、したがって基盤となるデータセット上で実行されます。
トランザクションのロールバック
トランザクション内では、すべてのステートメントがデータベースにコミットされるか、何もコミットされないかのいずれかです。トランザクション内で行われたステートメントと変更をデータベースに適用する代わりに放棄することを、トランザクションの「ロールバック」と呼びます。
トランザクションは自動的に、または手動でロールバックできます。MySQLは、トランザクション内のステートメントのいずれかでエラーが発生した場合、または問題を回避するために他のシナリオでトランザクションを自動的にロールバックします。
現在のトランザクション中に与えられたステートメントを手動でロールバックするには、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>
は、これらのいずれかです(詳細については前述)。
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
(MySQLのデフォルト動作モード)SERIALIZABLE
コマンドを発行する際に、異なるスコープに影響を与えるために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保証に大きく貢献しているため、多くのシナリオで依然として役立ちます。それらをいつ使用するか、どの分離レベルが適切か、そして自動ロールバックを回避する方法を理解することは、投資する価値のある知識です。