共有

はじめに

トランザクションは、データベースが関連するステートメントをシステムによって単一の単位として実行されるようにグループ化する方法です。ステートメントは単一の単位として処理され、すべての構成要素が正常に完了するか、元の状態に戻るかのいずれかになります。これは、単一の目標を達成するために複数の個別のステップを要する変更の整合性を維持する一つの方法です。

この記事では、トランザクションとは何か、そしてそれがどのように役立つかについて説明します。次に、MySQLがさまざまな方法でトランザクションを使用して、ステートメントがデータベースにどのように適用されるかを正確に制御する方法を見ていきます。

トランザクションとは?

トランザクションは、複数のステートメントを単一の操作として処理するためにグループ化し、分離する方法です。各コマンドがサーバーに送信されるたびに個別に実行されるのではなく、トランザクションではコマンドがまとめてバンドルされ、他のリクエストとは別のコンテキストで実行されます。

分離性は、トランザクションの重要な要素です。トランザクション内では、実行されたステートメントはトランザクション自体の環境にのみ影響します。トランザクションの内部からは、ステートメントはデータを変更でき、その結果はすぐに表示されます。外部からは、トランザクションがコミットされるまで変更は行われず、コミットされた時点でトランザクション内のすべてのアクションが一度に表示されるようになります。

これらの機能は、原子性(トランザクション内のアクションはすべてコミットされるか、すべてロールバックされるか)、および分離性(トランザクションの外部ではコミットされるまで何も変更されず、内部では各ステートメントが即座に結果をもたらす)を提供することにより、データベースがACID準拠を達成するのに役立ちます。これらを合わせることで、データベースが整合性を維持するのに役立ちます(部分的なデータ変換が発生しないことを保証することで)。さらに、トランザクション内の変更は、不揮発性ストレージにコミットされるまで成功として返されず、これにより永続性が提供されます。

これらの目標を達成するために、トランザクションはさまざまな戦略を採用し、異なるデータベースシステムは異なる方法を使用します。MySQLは、データスナップショットを使用してデータベースがこれらのアクションを実行できるようにする多版型同時実行制御 (MVCC)と呼ばれるシステムを使用します。これらすべてのシステムは、現代のリレーショナルデータベースの基本的な構成要素の一つであり、クラッシュ耐性のある方法で複雑なデータを安全に処理できるようにします。

整合性障害の種類

人々がトランザクションを使用する理由の一つは、データの整合性とそれが処理される環境に関する特定の保証を得るためです。整合性はさまざまな方法で破られる可能性があり、それがデータベースがそれらを防ぐ方法に影響を与えます。

トランザクションの実装に応じて、整合性の欠如が発生する主な方法が4つあります。これらのシナリオが発生する可能性のある状況に対する許容度が、アプリケーションでのトランザクションの使用方法に影響を与えます。

ダーティリード

ダーティリードは、トランザクション内のステートメントが他の進行中のトランザクションによって書き込まれたデータを読み取ることができる場合に発生します。これは、トランザクションのステートメントがまだコミットされていなくても、それらが読み取られ、したがって他のトランザクションに影響を与える可能性があることを意味します。

これは、トランザクションが互いに適切に分離されていないため、整合性の重大な違反と見なされることがよくあります。データベースにコミットされない可能性のあるステートメントが、他のトランザクションの実行に影響を与え、その動作を変更する可能性があります。

ダーティリードを許可するトランザクションは、結果のデータの整合性について合理的な主張をすることはできません。

ノンリピータブルリード

ノンリピータブルリードは、トランザクション外部でのコミットがトランザクション内で参照されるデータを変更する場合に発生します。この種の問題は、トランザクション内で同じデータが2回読み取られ、各インスタンスで異なる値が取得される場合に認識できます。

ダーティリードと同様に、ノンリピータブルリードを許可するトランザクションは、トランザクション間の完全な分離性を提供しません。違いは、ノンリピータブルリードでは、トランザクションに影響を与えるステートメントが実際にトランザクションの外部でコミットされていることです。

ファントムリード

ファントムリードは、トランザクション内でクエリが2回目に実行されたときに返される行が異なる場合に発生する、ノンリピータブルリードの特定のタイプです。

たとえば、トランザクション内のクエリが最初に実行されたときに4行を返し、2回目に5行を返す場合、これはファントムリードです。ファントムリードは、クエリを満たす行の数を変更するトランザクション外部でのコミットによって引き起こされます。

直列化異常

直列化異常は、複数のトランザクションが同時にコミットされた結果が、それらが一つずつコミットされた場合とは異なる結果になる場合に発生します。これは、トランザクションが競合を解決せずに、同じテーブルまたはデータをそれぞれ変更する2つのコミットを許可するたびに発生する可能性があります。

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

トランザクションは「万能」なソリューションではありません。シナリオによって、パフォーマンスと保護の間で異なるトレードオフが求められます。幸いなことに、MySQLでは必要なトランザクション分離のタイプを指定できます。

ほとんどのデータベースシステムで提供される分離レベルは次のとおりです。

リードアンコミッテッド

リードアンコミッテッドは、データの整合性と分離性の維持に関して最も保証が少ない分離レベルです。read uncommittedを使用するトランザクションには、一度に複数のステートメントをコミットする機能や、間違いが発生した場合にステートメントをロールバックする機能など、トランザクションに頻繁に関連付けられる特定の機能がありますが、整合性が損なわれる可能性のある多くの状況を許可します

read uncommitted分離レベルで設定されたトランザクションは、以下を許可します。

  • ダーティリード
  • ノンリピータブルリード
  • ファントムリード
  • 直列化異常

この分離レベルは、データの整合性と分離性に関してほとんど保証を提供しないため、一般的には推奨されません。「すべてか無か」のコミットメントモデルでステートメントをグループ化する必要があるが、整合性の保証が不要な場合(非常にまれなケース)に主に役立ちます。

リードコミッテッド

リードコミッテッドは、特にダーティリードから保護する分離レベルです。トランザクションがread committed整合性レベルを使用する場合、未コミットのデータはトランザクションの内部コンテキストに決して影響を与えません。これにより、未コミットのデータがトランザクションに影響を与えることがないようにすることで、基本的な整合性レベルが提供されます。

Although read committedread 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;
statements
COMMIT;

より具体的な例として、1000ドルをある口座から別の口座へ送金しようとしていると想像してみてください。このお金が常に2つの口座のいずれか一方にあり、両方には決してないことを確認したいとします。

この送金をカプセル化する2つのステートメントを、次のようなトランザクションで囲むことができます。

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;
COMMIT;

ここでは、id = 1の口座から1000ドルが引き出されることは、同時にid = 2の口座に1000ドルが入金されることなしにはありません。これらの2つのステートメントはトランザクションの内部で順次実行されますが、同時にコミットされ、したがって基盤となるデータセット上で実行されます。

トランザクションのロールバック

トランザクション内では、すべてのステートメントがデータベースにコミットされるか、何もコミットされないかのいずれかです。トランザクション内で行われたステートメントと変更をデータベースに適用する代わりに放棄することを、トランザクションの「ロールバック」と呼びます。

トランザクションは自動的に、または手動でロールバックできます。MySQLは、トランザクション内のステートメントのいずれかでエラーが発生した場合、または問題を回避するために他のシナリオでトランザクションを自動的にロールバックします。

現在のトランザクション中に与えられたステートメントを手動でロールバックするには、ROLLBACKコマンドを使用できます。これにより、トランザクション内のすべてのステートメントがキャンセルされ、実質的にトランザクションの開始時点に戻されます。

例えば、以前使用していた銀行口座の例を使用していると仮定して、UPDATEステートメントを発行した後で、誤って金額を間違えたり、間違った口座を使用したりしたことが判明した場合、変更をコミットする代わりにロールバックすることができます。

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE 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 accounts
SET balance = balance - 1500
WHERE id = 1;
/* Set a save point that we can return to */
SAVEPOINT save_1;
UPDATE accounts
SET balance = balance + 1500
WHERE 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 accounts
SET balance = balance + 1500
WHERE id = 4;
COMMIT;

ここでは、これまでのトランザクションで行ったすべての作業を失うことなく、犯した間違いから回復できます。ロールバック後、正しいステートメントを使用して計画通りにトランザクションを続行します。

トランザクションの分離レベルの設定

トランザクションに希望する分離レベルを設定するには、ISOLATION LEVEL句を含むSET TRANSACTIONステートメントを使用できます。SET TRANSACTIONステートメントを使用すると、トランザクションの分離レベルと読み書き権限を変更できます。

デフォルトでは、SET TRANSACTIONステートメントは、次に開始されるトランザクションのプロパティのみに影響します。これは、START TRANSACTIONまたはBEGINステートメントの前に記述する必要があります。基本構文は次のようになります。

SET TRANSACTION ISOLATION LEVEL <isolation_level>;
START TRANSACTION;
statements
COMMIT;

<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 COMMITTED
START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE 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 accounts
SET balance = balance - 1000
WHERE id = 2;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 3;
COMMIT;

トランザクションを連鎖させることで、毎回トランザクションレベルを明示的に設定したり、セッションやグローバルのデフォルトを変更したりすることなく、複数のトランザクションを作成するのに役立ちます。

まとめ

トランザクションは、データを一貫した状態に保つのに役立ついくつかの便利な機能を提供します。ただし、さまざまな分離レベルには、留意すべきいくつかのトレードオフがあります。ユースケースに適した保護レベルを決定するには、ある程度の検討と考察が必要です。これは、トランザクションが長時間実行される場合に特に当てはまります。コミットが発生する前にデータベースが大幅に変更される可能性があり、ロールバックやより多くの手動作業につながる可能性があるためです。

欠点があるにもかかわらず、トランザクションは、リレーショナルデータベースが提供すると期待されているACID保証に大きく貢献しているため、多くのシナリオで依然として役立ちます。それらをいつ使用するか、どの分離レベルが適切か、そして自動ロールバックを回避する方法を理解することは、投資する価値のある知識です。

著者について
Justin Ellingwood

ジャスティン・エリングウッド

ジャスティンは2013年からデータベース、Linux、インフラストラクチャ、開発者ツールについて執筆しています。現在は妻と2匹のウサギとともにベルリンに住んでいます。彼は通常、三人称で書く必要がないため、関係者全員にとって安心です。
© . All rights reserved.