不整合が起きてはならない場合、トランザクションはシリアライザブル

(更新日: 2015/05/08)

リレーショナルデータベースが優れている点はトランザクションをサポートしている点です。トランザクションは手続きが一貫性ある形で実行されることを保証してくれます。しかし、トランザクションを使えばOK、という物ではありません。

もしトランザクションさえ使っていればOKと思っていた方はトランザクション分離レベルを理解してください。

トランザクションアイソレーション(分離)レベル

トランザクションには種類があります。

トランザクション分離(アイソレーション)レベル (Wikipedia)

ANSI/ISO SQL標準で定められている分離レベルは、下記の4種類で定義されている。

  • SERIALIZABLE ( 直列化可能 )
複数の並行に動作するトランザクションそれぞれの結果が、いかなる場合でも、それらのトランザクションを時間的重なりなく逐次実行した場合と同じ結果となる.このような性質を直列化可能性(Serializability)と呼ぶ.SERIALIZABLEは最も強い分離レベルであり、最も安全にデータを操作できるが、相対的に性能は低い。ただし同じ結果とされる逐次実行の順はトランザクション処理のレベルでは保証されない。
  • REPEATABLE READ ( 読み取り対象のデータを常に読み取る )
ひとつのトランザクションが実行中の間、読み取り対象のデータが途中で他のトランザクションによって変更される心配はない。同じトランザクション中では同じデータは何度読み取りしても毎回同じ値を読むことができる。
ただし ファントム・リード(Phantom Read) と呼ばれる現象が発生する可能性がある。ファントム・リードでは、並行して動作する他のトランザクションが追加したり削除したデータが途中で見えてしまうため、処理の結果が変わってしまう。
  • READ COMMITTED ( 確定した最新データを常に読み取る )
他のトランザクションによる更新については、常にコミット済みのデータのみを読み取る。 MVCC はREAD COMMITTEDを実現する実装の一つである。
ファントム・リード に加え、非再現リード(Non-Repeatable Read)と呼ばれる、同じトランザクション中でも同じデータを読み込むたびに値が変わってしまう現象が発生する可能性がある。
  • READ UNCOMMITTED ( 確定していないデータまで読み取る )
他の処理によって行われている、書きかけのデータまで読み取る。
PHANTOMNON-REPEATABLE READ 、さらに ダーティ・リード(Dirty Read) と呼ばれる現象(不完全なデータや、計算途中のデータを読み取ってしまう動作)が発生する。トランザクションの並行動作によってデータを破壊する可能性は高いが、その分性能は高い。

このようにISO標準で分離レベルは定義されています。

 

データベースのトランザクション分離レベル

例えば、PostgreSQLは3つトランザクションをサポートし、デフォルトのトランザクション分離レベルはREAD COMMITTEDです。MySQLはサーバー起動時にデフォルトのトランザクション分離レベルが指定可能になっています。14.2.4 Consistent Nonlocking Readsに書かれている通り、MySQLもデフォルトのトランザクション分離レベルはREPEATABLE READです。

PostgreSQLマニュアルのテーブルを見ると違いが分かりやすいです。

分離レベル ダーティリード 反復不能読み取り ファントムリード
リードアンコミッティド 可能性あり 可能性あり 可能性あり
リードコミッティド 安全 可能性あり 可能性あり
リピータブルリード 安全 安全 可能性あり
シリアライザブル 安全 安全 安全

リードコミッテドの場合、

ファントム・リード に加え、非再現リード(Non-Repeatable Read)と呼ばれる、同じトランザクション中でも同じデータを読み込むたびに値が変わってしまう現象が発生する可能性がある。

とWikipediaに書いてある通り不整合が発生します。簡単に言うと

  • ファントム・リード – タイミング次第で見えなかったデータが見えるよう、見えないようになること
  • 非再現リード(ノンリピータブル・リード) – 同じデータの読み込みができなくなること

ファントム・リードやノンリピータブル・リードは複雑なクエリでないと起きない、と思っている方も居るかも知れません。

 

不整合が起きる例

トランザクションの不整合は単一レコードへのアクセスでも起きてしまいます。例えば、セッションIDのデータベースにデフォルトのリードコミッテド分離レベルを利用した場合、ブラウザから複数の接続、複数のブラウザタブ/クライアントからの複数接続(単一ページでもWebブラウザは複数の接続を利用してWebサーバーに接続してデータを取得する)が在るため、1つのデバイスからのアクセスでも不整合が起こりえます。

具体的には

  • 接続Aがトランザクションを開始しセッションデータを読み取る
  • 接続Bがトランザクションを開始しセッションデータを読み取る

が同時に起こる可能性があります。Webアプリがアクセス回数をセッションデータに保存している場合、接続A/接続Bの両方が同じアクセス回数のデータにアクセスし、両方が1つカウンタを増加しても、どちらかの結果しか残りません。

つまり、接続A/接続Bがアクセスを開始する前のカウンタが100であったなら、接続A/接続Bの処理が終わった時点で102にならなければならないのですが、接続A/接続Bのアクセスが同時なら101になる、ということです。

トランザクションを使っているのに不整合が起きるなんて!と思うかも知れませんが、同時実効性を上げるにはリードコミッテド分離レベルが向いています。多くのアプリケーションではリードコミッテド分離レベルで十分であることが多い(SELECTでコミット済みデータが読み取れれば良い)からです。

 

データの不整合を防ぐ

データの不整合を防ぐにはリードコミッテドより高い分離レベルのトランザクション、リピータブルリードまたはシリアライザブルを利用する必要があります。

例えば、

  • 接続Aがトランザクションを開始しセッションデータを読み取る
  • 接続Bがトランザクションを開始しセッションデータを読み取る

が同時に発生した場合でも、シリアライザブル分離レベルのなら遅く始まった方のトランザクションが早く始まった方のトランザクションが終るまでブロックされ、不整合が発生しません。

よく分からない場合はシリアライザブルを利用すると不整合は発生しません。トランザクションを利用した処理がシリアル化(直列化) されるため、常に整合性が取れた結果になります。同時実効性が犠牲になるだけなので、クリティカルなデータの更新/参照にはシリアライザブルを使うと良いでしょう。勿論、違いを理解してリピータブルリードで十分な場合(トランザクション内で同じ値であれば良い場合)、これを利用した方が同時実効性/パフォーマンスは向上します。

リピータブルリード、シリアライザブルの分離レベルではトランザクションが失敗する可能性があります。アプリケーションはトランザクションのコミットが失敗した場合、再度トランザクションをやり直す、などのコードを用意しておかなければなりません。

参考:

 

まとめ

トランザクションはデータの不整合を防ぐ為の仕組みです。しかし、トランザクション分離レベルには複数のレベルがあり、デフォルトの分離レベル(READ COMMITTED)ではデータの整合性を完全に保証することができない場合があります。

トランザクションは「使いさえすれば整合性が保たれる」ものではありません。必要に応じてリピータブルリード/シリアライザブル分離レベルを使用しないと、残高がおかしくなったり、予約でオーバーブッキングしてしまう、といった問題が発生してしまいます。

トランザクション分離レベルの知識はシステム開発者の必須基礎知識です。確実に押さえておきましょう。

 

追記

最初、MySQLのデフォルトトランザクション分離レベルを間違えて書いていたので修正しました。序でに自分の記憶のリフレッシュも兼てデフォルトの分離レベルをまとめてみました。細かい動作はマニュアルで確認してください。色々条件/制限があります。

 

 

Comments

comments

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です