概要
分離レベルは、トランザクションが並行して実行される際の相互干渉の程度を表すものです。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データベースでは、読み取りコミットとシリアライズ可能の2種類の分離レベルのみが実装されています。ユーザーが厳密読み取り可能の分離レベルを指定した場合、実際にはシリアライズ可能が使用されます。同様に、ユーザーが読み取り未コミットの分離レベルを指定した場合、実際には読み取りコミットが使用されます。つまり、OceanBaseデータベースの厳密読み取り可能の分離レベルの方が厳密であり、幻覚読み取りの異常が発生することはありません。OceanBaseデータベースの読み取りコミットでは、ダーティーリードの異常は発生しませんが、不厳密読み取り可能および幻覚読み取りの異常が発生する可能性があります。一方、シリアライズ可能では、ダーティーリード、不厳密読み取り可能、および幻覚読み取りの異常は発生しません。
読み取りコミット
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標準と一致 | サポート、フェントムリードなし | サポート、厳密な直列化は保証される |