データベースの分離レベルについて纏めます。
1 分離レベルとは
OracleやSQLServer等のリレーショナルデータベースはACID属性という概念を基に設計されているという話は聞いたことがある人が多いと思います。
それぞれ
Atomicity(原始性)
データの処理はトランザクションという単位で制御されてcommitかrollbackすることで処理が終了するということです。
Consistency(一貫性)
データが書き換えられない限りいつ、どこから取得しても結果は同じになる。
updateのSQLを実行した後でその内容をselectしたら必ずupdateした内容が取得できるということです。
Isolation(独立性)
今回のメインテーマです。
詳しくは後述しますが、あるトランザクションで操作した内容はほかのトランザクションに影響を与えない(=独立している)ということです。
Durability(永続性)
データが失われることがないようにする。
障害などでデータが失われてもバックアップやログなどから復旧する手段を用意しておくべきということです。
このページの主題である分離レベルとはConsistencyを実現するための設定です。
分離レベルの設定によっては完全にほかのトランザクションから独立できたり他のトランザクションから影響を受けるようになったりするようになります。
この分離レベルを低くする(他のトランザクションからの影響を受けやすくする)と独立性は弱くなりますが、その分パフォーマンスは向上します。必要な独立性はどれなのかを考えてDB設計をする必要があるわけですね。
2 分離レベルの種類
分離レベルは高い順から紹介すると
- serializable
- repeatable read
- read committed
- read uncommitted
の4つがあります。
この4つをそれぞれ説明するには先にトランザクションが他のトランザクションによって受ける現象の説明が必要です。
この現象には3種類あるので、それぞれ説明します。(覚えることが多いですね。。。)
Dirty read
他のトランザクションがcommitしていない情報を読み込む現象。
読み込んでしまった後で確実にcommitしてくれるならまだいいのですが、問題は他のトランザクションがロールバックしてしまったケースです。
他のトランザクションがinsertしたけどまだcommitしていないレコードを読み込んでupdateしようとするとします。
update実行前に他トランザクションがロールバックした場合、存在しないレコードをupdateしようとしてエラーになってしまいます。
update実行後にロールバックされると更新したはずのレコードが見当たらないということになります。
このようなことを防ぐためにこのDirty readだけは最低限防ぐようにするべきでしょう。
fuzzy read
Non-repeatable readとも呼ばれます。これはあるトランザクションの処理中に他のトランザクションが同一レコードに対して更新を掛けてcommitした場合、その内容がこちらにも反映されるという現象です。
つまり他のトランザクションとupdateの対象レコードが重複した時に発生します。
対策しなければ他のトランザクションの更新を上書き、もしくは自分の更新内容が上書きされてしまい、思わぬ不具合を生むことになります。
この場合、楽観排他や悲観排他といった排他制御を取り入れることで対応できます。
処理が競合した場合は自分の処理を一旦ロールバックして取りやめる方法です。
Phantom Read
これはトランザクションで更新予定のテーブルに他のトランザクションがレコードを追加・削除した場合にその内容がこちらにも反映される現象です。
個人的にこの現象は発生しないとマズいケースもあるのかなと思っています。
例えば、IDというカラムが主キーのテーブルでIDは1から順に連番が振られているとします。
あるトランザクションがこのテーブルにID=2のレコードをinsertしようとしている時に他のトランザクションが先にID=2のレコードをinsertしてcommitしてしまった場合、自分のinsertは主キーが重複してエラーになってしまいます。
Phantom Readが発生するなら事前にIDの重複を検知してロールバックすることができますしそもそもシーケンスなどを使用してIDを設定しているなら自分のinsertはID=3でcommitすることも可能になります。
勿論レコードの数によって結果が変わる処理等では想定外の結果になるので対策は必要になります。
ところでファントムリードって名前カッコいいですよね!
これら3つの現象を防ぐかどうかの設定値が上記で紹介したserializable、repeatable read、read committed、read uncommittedの4つです。
設定の効果は下記の表に示す通りです。
分離レベル\現象 | Dirty read | fuzzy read | Phantom Read |
---|---|---|---|
read uncommitted | 発生する | 発生する | 発生する |
read committed | 発生しない | 発生する | 発生する |
repeatable read | 発生しない | 発生しない | 発生する |
serializable | 発生しない | 発生しない | 発生しない |
read uncommitedに設定してしまうとその名前の通りcommitされていないデータを読み込むDirty readが発生してしまうので基本的にこれは避けるべきでしょう。
自分は仕事でWEBシステムを扱っていますが、WEBシステムで使用するDBはread committedで設定することが多いようです。
WEBシステムは不特定多数の人が使用するのでパフォーマンスを重視しなければならず、あまり重い設定はできないのが大きな理由であとは排他制御等を採用してのロジック側で対応するケースが多いのかと。
ちなみにserializableについては全ての減少を防ぐ最強の設定ですが、実際にどうやって防ぐのかというとトランザクションの同時実行を不可能にするようです。あるトランザクションが処理中に他のトランザクションが~ということがそもそも発生しなくなるので不整合は発生しなくなりますがそりゃ処理も重くなりますね。。。
まとめ
トランザクション分離レベルによってデータの不整合の発生を段階的に防ぐことができる。
より多くの不整合を防ごうとするとその分処理に時間がかかるので許容できる不整合とできない不整合を考え、パフォーマンスとのバランスを考えて設定する必要がある。