概要
分離レベルとは、トランザクションが並行して実行される際の相互干渉の程度を示すものです。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種類の分離レベルが定義されています。これらの分離レベルは以下のとおりです:
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標準と一致 | サポート、ファントムリードなし | サポート、厳密な一貫性シリアライゼーションが保証される |