概要
分離レベルは、トランザクションの並行実行時における相互干渉の程度を表すものです。ANSI/ISO SQL標準(SQL 92)では、トランザクションの実行過程で回避すべき異常な状況に基づいて4つの分離レベルが定義されています。分離レベルが高いほど、トランザクション間の相互影響は小さくなり、許容される異常な状況も少なくなります。最も高い分離レベルである直列化(Serializable)では、いかなる異常な状況も発生することは許されません。 これら回避すべき異常な状況には以下のものが含まれます:
ダーティーリード(Dirty Read):あるトランザクションが、他のトランザクションが未コミットのデータを読み取ること。
不可再現読み取り(Non Repeatable Read):一度読み取った特定の行データについて、再度クエリを実行したときに、その行データが変更または削除されていることが判明すること。例:
select c2 from test where c1=1;最初のクエリでc2の結果は1ですが、再度クエリを実行すると、他のトランザクションによってc2の値が変更されているため、c2の結果は2になります。ファントムリード(Phantom Read):リクエストの実行中に、同じ検索条件を再度実行したとき、結果セットから別のコミット済みトランザクションによって新規に挿入された、条件を満たす行が読み取られること。
分離レベルの種類
ANSI/ISO SQL標準(SQL 92)では、4種類の分離レベルが定義されています。これら4種類の分離レベルは以下のとおりです:
読み取り未コミット(Read Uncommitted)
読み取りコミット(Read Committed)
厳密読み取り返し(Repeatable Read)
トランザクション可能(Serializable)
4種類の分離レベルと、上記の異常状況に対する許容度は以下のとおりです:
分離レベル |
スティンガー読み取り |
不可再現読み取り |
ファントム読み取り |
|---|---|---|---|
| 読み取り未コミット | 可能 | 可能 | 可能 |
| 読み取りコミット済み | 不可能 | 可能 | 可能 |
| 可再現読み取り | 不可能 | 不可能 | 可能 |
| シリアライズ可能 | 不可能 | 不可能 | 不可能 |
OceanBaseデータベースの分離レベル
OceanBaseデータベースは現在、以下の分離レベルをサポートしています:
Oracleモード
Read Committed
Serializable
MySQLモード
Read Uncommitted
Read Committed
Repeatable Read
Serializable
この分離レベルはOracleデータベースのSerializableに類似しており、厳密な意味でのSerializableではありません。
OceanBaseデータベースのデフォルトの分離レベルはRead Committedです。
実際にOceanBaseデータベースでは、Read CommittedとSerializableの2種類のみが実装されています。ユーザーがRepeatable Readの分離レベルを指定した場合、実際に使用されるのはSerializableです。同様に、ユーザーがRead Uncommittedの分離レベルを指定した場合、実際に使用されるのはRead Committedです。つまり、OceanBaseデータベースのRepeatable Read分離レベルの方がより厳格であり、幻読の異常が発生することはありません。OceanBaseデータベースのRead Committedでは、ダーティーロードの異常は発生しませんが、不重复読み取りと幻読の異常が発生する可能性があります。一方、Serializableでは、ダーティーロード、不重复読み取り、および幻読の異常はすべて発生しません。
コミット済み読み取り
OceanBaseデータベースのコミット済み読み取り分離レベルでは、各 SELECT ステートメントの実行時には、そのステートメント以前にコミット済みのトランザクションによって変更されたデータのみが読み取れ、ステートメントの実行中に新たにコミットされたり、並行するトランザクションによって変更されたりしたデータは読み取れません。これは、各ステートメントの実行前に現在のデータベースの最新スナップショットを取得しているかのような動作です。スナップショットにはコミット済みのデータのみが記録されているため、ダーティリードの現象は発生しません。しかし、各ステートメントの実行前に新しいスナップショットが取得されるため、同一トランザクション内で連続する2つの SELECT ステートメントが異なるデータを見る可能性があります。つまり、コミット済み読み取り分離レベルでは、不重复読み取りや幻覚読み取りの異常状況を回避することはできません。
UPDATE、DELETE、SELECT FOR UPDATE などの更新操作についても、ターゲット行を検索する際の動作は SELECT と同じです。つまり、ステートメント開始前にコミット済みの行バージョンしか見つけることができません。そのバージョンが更新操作の述語条件を満たさない場合は、その行を直接スキップし、条件を満たす場合にのみ更新を試みます。しかし、現在のトランザクション(以下、トランザクションAと略称)が述語条件を満たすターゲット行の更新を試みたとき、その行は別の並行トランザクション(以下、トランザクションBと略称)によって既に更新されている可能性があります。このような場合、トランザクションBがまだ終了していない場合、トランザクションAはトランザクションBのコミットまたはロールバックを待機する必要があります。トランザクションBがロールバックした場合、トランザクションAはターゲット行の更新を続けることができます。トランザクションBがコミットした場合、トランザクションAはそのステートメントを再実行し、ステートメントのスナップショットを再取得して、トランザクションBによって更新されたバージョンを読み取ります。そのバージョンが依然として述語条件を満たす場合、その上で更新を行います。
不重复読み取りまたは直列化可能
OceanBaseデータベースの直列化可能(または不重复読み取り)分離レベルでは、トランザクションの最初のステートメントが現在のデータベースのスナップショットをトランザクションスナップショットとして取得します。その後の SELECT ステートメントはすべて、トランザクションスナップショットに基づいてデータを読み取ります。これにより、トランザクションスナップショット以前にコミット済みのトランザクションによって変更されたデータのみが読み取れ、トランザクションの実行中に新たにコミットされたり、並行するトランザクションによって変更されたりしたデータは読み取れません。各ステートメントが同じトランザクションスナップショットを使用するため、トランザクション内では常に一貫したデータが見え、不重复読み取りや幻覚読み取りの異常状況は発生しません。
UPDATE、DELETE、SELECT FOR UPDATE などの更新操作についても、ターゲット行を検索する際の動作は SELECT と同じです。つまり、トランザクションスナップショットを取得する前にコミット済みの行バージョンしか見つけることができません。そのバージョンが更新操作の述語条件を満たさない場合は、その行を直接スキップし、条件を満たす場合にのみ更新を試みます。しかし、現在のトランザクション(以下、トランザクションAと略称)がターゲット行を検索したとき、その行は別の並行トランザクション(以下、トランザクションBと略称)によって既に更新されている可能性があります。このような場合、トランザクションBがまだ終了していない場合、トランザクションAはトランザクションBのコミットまたはロールバックを待機する必要があります。トランザクションBがロールバックした場合、トランザクションAは最初に見つけた行の更新を続けることができます。トランザクションBがコミットした場合、トランザクションAは古いスナップショットに基づいて更新を行うことはできません。そうすると、更新損失(Lost Update)が発生する可能性があるため、トランザクションAはロールバックするしかありません。この場合、OceanBaseデータベースは以下のエラーメッセージを返します:
Oracleモード:
ORA-08177: can't serialize access for this transactionMySQLモード:
ERROR 6235 (25000): can't serialize access for this transaction
業務層では、書き込み競合によりトランザクションがロールバックされる可能性があることを考慮し、トランザクションの再試行ロジックを準備する必要があります。トランザクションが複雑で再試行のコストが高く、かつ業務上すべてのステートメントが一貫したデータを見る必要がない場合は、コミット済み読み取り分離レベルの使用を推奨します。
OceanBaseデータベースのシリアライズ可能分離レベルの制限
SQL標準では、シリアライズ可能分離レベルは、ダーティーロード、非再現可能読み取り、およびファントムリードの異常状況を回避するだけで済みます。しかし、シリアライズ可能の厳密な定義とは、成功してコミットされた任意の2つの並行トランザクションを順序付きで実行し、一方のトランザクションをもう一方のトランザクションの後に配置すること、すなわちトランザクションの並列実行結果が何らかの直列実行結果と同じである必要があることです。OracleやPostgreSQL 9.0及びそれ以前のバージョンと同様に、OceanBaseデータベースのシリアライズ可能分離レベルでは厳密なシリアライゼーションは保証されません。つまり、トランザクションの実行結果がいかなる直列実行パターンの結果とも同じでない可能性があります。その中でも、ライトスキュー(Write Skew)は典型的な例です。 テーブル T1(num int) と T2(num int) が存在すると仮定します。両テーブルは初期状態ではデータがありません。この時、トランザクション1(Trx1)とトランザクション2(Trx2)で以下の順序でコマンドを実行します。
Trx1 Trx2
BEGIN;
INSERT INTO T2 SELECT COUNT(*) FROM T1;
BEGIN;
INSERT INTO T1 SELECT COUNT(*) FROM T2;
COMMIT;
COMMIT:
Trx1とTrx2が取得したスナップショットでは、両テーブルの COUNT はどちらも0であるため、最終的にはテーブルT1とテーブルT2に num=0 の行が挿入されます。しかし、トランザクション1とトランザクション2を直列実行する場合、Trx1->Trx2 であれ Trx2->Trx1 であれ、最終的にはテーブルに num=0 が挿入され、もう一方のテーブルに num=1 が挿入されるはずです。OceanBaseデータベースが現在厳密なシリアライズ可能を保証できない理由は、読み取り操作でロックを取得しない、読み書きが排他的でない、およびトランザクションコミット時に読み書き競合が循環していないかチェックしないためです。ほとんどの実際のアプリケーションシナリオでは、ダーティーロード、ファントムリード、非再現可能読み取りが発生しない限り、ビジネス要件を満たすことができます。ビジネスで厳密なシリアライズ可能が必要な場合は、SELECT FOR UPDATE のように、読み取り操作に対して明示的にロックをかけることができます。
注意
シリアライズ可能分離レベルでは、トランザクション開始後、トランザクションのスナップショットが保持され、トランザクションコミット後にスナップショットが回収されます。そのため、シリアライズ可能分離レベルでは、トランザクションを適時にコミットする必要があります。そうでない場合、ディスク使用量の増加や読み取り速度の低下といった問題が発生する可能性があります。
OceanBaseデータベースの分離レベルと他のデータベースとの比較
データベース |
読み取り未コミット(Read Uncommitted) |
読み取りコミット(Read Committed) |
繰り返し読み取り可能(Repeatable Read) |
厳密に直列化可能(Serializable) |
|---|---|---|---|---|
| OceanBase | 構文のみサポート | サポート、SQL標準と一致 | サポート、フェントムリードなし | サポート、厳密な直列化は保証されない |
| Oracle | 構文非サポート | サポート、SQL標準と一致 | 構文非サポート | サポート、厳密な直列化は保証されない |
| MySQL | サポート、ダーティデータを読む可能性あり | サポート、SQL標準と一致 | サポート、フェントムリードなし | サポート、厳密な直列化が保証される |
| PostgreSQL 9.1以前バージョン | 構文はサポート、実際はRead Committed | サポート、SQL標準と一致 | サポート、フェントムリードなし | サポート、厳密な直列化は保証されない |
| PostgreSQL 9.1以降バージョン | 構文はサポート、実際はRead Committed | サポート、SQL標準と一致 | サポート、フェントムリードなし | サポート、厳密な直列化が保証される |