ACID特性はデータベース論の基礎の一つです。ACIDではデータベースの信頼性を保つために必要とされる4つの属性を定義しています。原子性(Atomicity)、一貫性(Consistency)、分離性(Isolation)、そして永続性(Durability)です。4つの属性はいずれも重要ですが、とりわけ分離性については最も柔軟に解釈されています。ほとんどのデータベースがいくつかの分離レベルを選択できるようにしていますし、最近は多くのライブラリがより極め細やかな分離レベルを作成するレイヤを追加しています。このように広範囲の分離レベルが存在している主な原因は、緩い分離レベルによって拡張性や性能が数桁のオーダーで異なることにつながるからである。
シリアライズ可能というのは最も古典的で高い分離レベルであり、一般的に使うことが出来るもので、多くの人がその単純なプログラミング・モデルに惹かれてこれを選択します。特定のリソースに対しては同時に一つのトランザクションしか実行出来ないので、リソースに関係する多くの潜在的な問題が除去されます。しかし、多くのアプリケーション(とりわけWebアプリケーション)はユーザの観点からすると現実的ではないとの理由でこの高い分離レベルを採用することが出来ません。少なからぬ数のユーザを抱えるアプリケーションでは共有リソースに対するアクセスにおいて即座に処理の遅延を経験することなり、すぐにそのアプリケーションを利用するユーザ数は少なくなることでしょう。Webのような巨大な分散データ・ソースでは緩やかな一貫性(の保証)が一般的で、(eBayやAmazonのような)いくつかの成功を収めている巨大なWebベースのアプリケーションが楽観的で緩やかな一貫性(の保証)の方が古くからの悲観的なメカニズムより拡張性に富んでいることを示しました。この記事ではデータの一貫性に関する制約を緩和出来るようにすることで性能と拡張性を高める可能性を秘めている八つの異なる分離レベルを取り上げます。
同時実効制御のゴールはトランザクションが分離され、互いに他のトランザクションに干渉しないことを保証することです。より高度な分離レベルは潜在的な性能を犠牲にすることになります。ほとんどのリレーショナル・データベースは、書き込み操作に最適化されているので、悲観的なメカニズムを採用しています。悲観的なメカニズムはロックを使って操作をブロックするか、ある種の競合検出を行っています。テーブル、ページ、行などが更新された際に他のトランザクションが更新されようとしているリソースにアクセスするのを防ぐために悲観的ブロックが行われます。一方で楽観的なメカニズムではロックは一切行わず、トランザクションを分離する目的では競合検出だけに頼っています。競合検出は、楽観的なメカニズムで使われていて、全ての読み取り操作を許容しトランザクションの最後に一貫性を検証します。仮に競合が検出されればトランザクションはロールバックされるか再実行されます。ほとんどのWebサーバは読み取り操作に最適化されていますので楽観的なメカニズムを採用しています。全ての読み取り操作を許容することで、楽観的なメカニズムでは高い読み取り・書き込みスループットを達成しながらもリソースが継続的に変更されることのない状況でのデータの一貫性を確保しています。
Web開発者がプログラミング・モデルに与えられる制約をWeb開発者が理解し、システム・アーキテクトと開発者が必要なデータ一貫性を確保しつつ最も効果的な分離レベルを選択するための議論を行うのを助けるため、以下に分離レベルをリスト・アップしました。最も緩い分離レベル(Read Uncomitted:反復不能読み取り)から最も分離される(Serializability:シリアライズ可能)の順に示してあります。
1 Read Uncommitted
Read uncommitted という分離レベルではトランザクション間には低い分離性しか要求されません。全ての読み取り操作はあらゆるトランザクションによる保留中の(確定していない)書込み操作の結果を見ることになります(ダーティ・リード)。しかし、コミットされた書き込み操作はダーティ・ライトを回避するために順序が保たれていなければなりません。悲観的なメカニズムでは他(の操作)がコミットまたはロールバックされるまで競合する書き込み操作をブロックします。楽観的なメカニズムではロックはせず全ての操作を認めます。もしある接続がロールバックされたら、同一のデータに対して変更を行った他の接続も全てロールバックされます。このレベルでは検証なしの共有キャッシュが許容されます。この分離レベルは(読み取り専用のデータのような)トランザクションが不要な状況やデータベースへの排他的なアクセスによってのみ更新される場合に最適です。
利用例:オフラインからしか更新されないアーカイブ・データベース、もしくはトランザクションに含まれない監査/ロギング用のテーブル。
2 Read Committed
Read committed ではシステムのコミットされた状態を読み取り、現在のコネクションによる変更が結果に反映されている限りは検査なしで(混合された状態が)キャッシュされることもあります。悲観的なメカニズムでは Monotic View (後述)として実装されます。楽観的なトランザクションは全ての変更を独立して保持し、コミットされるまでは自身からのみ参照できるようにしておきます。この種の楽観的な分離方法は読み取り操作をブロックすることなく複雑な書き込み操作を許容し、検証もありません。共有キャッシュはコミットされた状態に対してのみ許可されます。この分離レベルは(更新前の)古くなった値が返されることが許容され、トランザクションには書き込み操作しか含まれない場合に最適です。
利用例:最新の投稿が反映されていなくても構わず投稿が互いに競合することがない、オンライン・フォーラム。
3 Monotonic View
Monotonic view は read committed を拡張したものでトランザクションは単調に増加していく*データベースの状態を監視します。このレベルで既に書き込み操作が開始されている場合、悲観的トランザクションは読み取り操作の間も待たされることになります。楽観的トランザクションは read committed のように自身による変更を独立して保持しますが、その変更が尚も正当なものであることを確認するためにキャッシュの内容を検証します。このレベルでは定期的な同期処理によってデータベースのクローンを作成することも許容されます。この分離レベルはトランザクションが要求されないか、もしくはトランザクションには書き込み操作しか含まれない場合に最適です。
利用例:一人からしか修正されることのないユーザ設定のテーブル。
4 Snapshot Reads
Snapshot Reads は monotonic view を拡張し問い合わせ結果はデータベースの一貫したスナップショットを反映していることを保証します。楽観的なメカニズムでは読み取り中の結果に影響が出ないように他の書き込み操作をブロックします。楽観的なメカニズムでは他の書き込み操作を許可した上でもし結果に何らかの変更があった場合には読み取り中のトランザクションに通知しそれをロールバックすることもあります。楽観的なメカニズムを実装するためには、同時に実行された書き込み操作によって結果に変更が生じないことを確認するため、読み取り操作の最後に検査を行う必要があります。そしてもし結果に変更が生じている場合には(トランザクションを)再実行するかロールバックされます。この検査では同一のテーブルに対する書き込み操作があったか、あるいは問い合わせ結果に変更がないかを確認するだけです。この楽観的な分離レベルでは容易に競合を検出でき書き込み操作に有利である一方、同時に読み取り操作が実行されることを許容します。このレベルは snapshot reads が提供され続けている限り、定期的な同期処理によってデータベースのクローンを作成することも許容されます。書き込み操作が少ないか同時に実行される読み取り操作との競合が想定されない場合でかつ問い合わせ結果の一貫性が要求される場合に最適です。
利用例:変更されるよりも問い合わせされる頻度が高く、最新の値のみを保持する通貨単位の変換表や参照用のテーブル。
5 Cursor Stability
Cursor Stability という分離レベルは read committed を拡張しており多くのリレーショナル・データベースにおいて分離レベルのデフォルト値となっている。この分離レベルでは、悲観的なトランザクションは読み込み段階でどのレコードを更新するのかを、別のSQL文であれ、明示する必要があります。これには通常'SELECT'による問い合わせ文の最後に'FOR UPDATE'句を追加することで対応します。このケースでは、他の競合する悲観的な読み取り・書き込みトランザクションは当該のトランザクションが完了するまでブロックされます。楽観的なトランザクションは更新した全てのレコード/エンティティのバージョン番号を追跡しコミット時に検証します。この方法は最も一般的な楽観的分離レベルであり、著名なオブジェクト-リレーションナル・マッピング用のライブラリの多くが提供している方法です。Java Persistence API では、(ローカル上での変更は問い合わせ結果に反映されないものの)FLUSH_ON_COMMITを使うことでこのレベルに近い状態を得ることが出来ます。そしてもし競合が検出された場合には OptimisticLockException が投げられます。さらにこの分離レベルは更新前に以前のバージョン番号やタイムスタンプと比較するために If-Match または If-Unmodified-Since というHTTPヘッダと合わせて使うことも出来ます。この分離レベルは(データベース内ではなく)外部の情報に基づき更新され、互いに変更を上書きすることのないようなエンティティに対して使うのが最適です。
利用例:共有される企業ディレクトリやWiki
6 Repeatable Read
Repeatable Read という分離レベルは cursor stability を拡張し、トランザクション内で読み取ったデータはそのトランザクションが継続している最中に変更、削除されないということを保証します。悲観的なトランザクションは全てのレコードのロックを取得しそれらのレコードに対する一切の更新トランザクションをブロックします。楽観的なトランザクションは全てのレコードやエンティティを追跡しコミット時にそれらが更新されていないことを検証します。この分離レベルはあるエンティティの状態が他のエンティティに影響し、トランザクションが読み取り操作と書き込み操作で構成されている場合に最適です。
利用例:注文監視データベース。このデータベースでは、あるエンティティから読み取られた値が他のエンティティの値を算出するのに利用される。
7 Snapshot Isolation
Snapshot isolation は snapshot reads と repeatable read を拡張し、トランザクションに含まれる全ての読み取り操作でデータベースの一貫性のあるスナップショットを読み取ることを保証しています。あるトランザクションに含まれる読み取り操作はそれがトランザクション内の最初の方で実行されようが最後の方で実行されようが常に同じ結果を得ることになります。これはファントム・リード(範囲指定問い合わせの結果が変わる)の発生を回避出来るという点で repeatable read とは異なります。この分離レベルは多くのリレーショナル・データベースでマルチバージョン・コンカレンシー・コントロールという形(あるいはSERIALIZABLEと呼ばれているかもしれません)でサポートされており、ロックと競合検出の組み合わせを使って悲観的なメカニズムとして実装されています。この分離レベルでは悲観的なメカニズム、楽観的なメカニズムのいずれかによる他のトンラクジョンとの競合の発生によってロールバックされることに対処する必要があります。悲観的なメカニズムはリソースをロックすることで競合の可能性を削減しますが、トランザクションのコミット時に結果をマージする必要があります。楽観的なメカニズムもまたマルチバージョン・コンカレンシー・コントロールを使いますが、他のトランザクションが競合する可能性のある操作をするのをブロックせず、その代わりに競合が検出されたトランザクションをロールバックします。この分離レベルは複数のレコードに対して読み取りと書き込みを行うようなトランザクションに最適です。
利用例:システムの状態に依存したルールを持ったワークフロー・システム。
8 Serializability
Serializability は snapshot isolation の拡張で全てのトランザクションが順次、一つずつ、実行されたかのように見えるという特徴があります。悲観的なメカニズムでは全ての問い合わせに対して範囲ロックを取得し、結果に影響を与える書き込み操作を阻止します。楽観的なメカニズムでは全ての問い合わせを追跡し、同時実行された書き込み処理が読み取り処理に影響を与えていないかどうかを検出するためにトンラザクションの最後で後方検証*または前方検証*を行います。そしてもし影響のある操作が見つかると一つのトランザクションを除く全ての競合トランザクションをロールバックします。この分離レベルではあるコミットされたトランザクションによって形成されたシステムの見かけ上の状態が変わることはありません**。この分離レベルは完全なデータの一貫性が求められるトランザクションに使われます。
利用例:新しい値を算出するために範囲指定問い合わせが必要となる会計システム。
まとめ
どの分離レベルが最適であるか判断するのを助けるため、以下にこの記事で概要を説明した分離レベルについてまとめてあります。
異なる分離レベルのトランザクション間で発生する可能性のある衝突の種類
ダーティー・ライト | ダーティー・リード | Mixed states | 一貫性のない読取 | 上書き更新 | ノンリピータブル・リード | ファントム・リード | 不一致 | |
Read Uncommitted | 不許可 | 許可 | 許可 | 許可 | 許可 | 許可 | 許可 | 許可 |
Read Committed | 不許可 | 不許可 | 許可 | 許可 | 許可 | 許可 | 許可 | 許可 |
Monotonic View | 不許可 | 不許可 | 不許可 | 許可 | 許可 | 許可 | 許可 | 許可 |
Snapshot Reads | 不許可 | 不許可 | 不許可 | 不許可 | 許可 | 許可 | 許可 | 許可 |
Cursor Stability | 不許可 | 不許可 | 許可 | 許可 | 不許可 | 許可 | 許可 | 許可 |
Repeatable Reads | 不許可 | 不許可 | 許可 | 許可 | 不許可 | 不許可 | 許可 | 許可 |
Snapshot Isolation | 不許可 | 不許可 | 不許可 | 不許可 | 不許可 | 不許可 | 不許可 | 許可 |
Serializability | 不許可 | 不許可 | 不許可 | 不許可 | 不許可 | 不許可 | 不許可 | 不許可 |
Optimistic requirements for different isolation levels:
キャッシュ | データの同期 | 楽観的な競合回避方法 | 用途 | 利用例 | |
Read Uncommitted | 許可 | 突発的 | ダーティー・ライトの検出 | 同時読み取りや書き込みのない状況 | アーカイブ |
Read Committed | 許可 | 突発的 | 競合検出なし | 単調な 読み取り/書き込 | Webフォーラム |
Monotonic View | 検証が必要 | 定期的 | 競合検出なし | 複合的な読み取り | ユーザ設定 |
Snapshot Reads | 検証が必要 | 定期的 | 読み取り前に変更内容を比較する | 一貫性のある読み取り | 参照用テーブル |
Cursor Stability | 許可 | 突発的 | 変更したエンティティのバージョンを比較する | CRUDサービス | ディレクトリ |
Repeatable Reads | 許可 | 突発的 | 変更したエンティティのバージョンを比較する | エンティティに対する読み取り/書き込み | 注文状況の監視 |
Snapshot Isolation | 検証が必要 | 定期的 | 読み取ったエンティティのバージョンを比較する | 同期化されたエンティティ | ワークフロー |
Serializability | 検証が必要 | 完全に同期 | 問い合わせ結果と変更内容を比較する | 完全な一貫性が要求されるデータ | 会計 |
データの一貫性はデータベース・アプリケーションにおいて極めて重要です。これによって開発者は並行実行環境においてデータの整合性を保つことが出来ます。serializability のような高い分離レベルを使うと単純なプログラミング・モデルになるものの、過剰なオーバーヘッド、操作のブロックやトランザクションのロールバックといったことの原因となりますので多くのアプリケーションには不要かもしれません。他のより最適な分離レベルについて知っておくと、開発者やシステム・アーキテクトはパフォーマンスとのトレードオフのバランスを上手くとった上で必要とれるデータの一貫性がどんなものであるか理解することが出来るでしょう。